diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 47ef7c07fc..0000000000 --- a/.flake8 +++ /dev/null @@ -1,5 +0,0 @@ -[flake8] -max-line-length = 88 -select = C,E,F,W,B,B9 -ignore = E203, E501, W503 -exclude = __init__.py diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/DISCUSSION_TEMPLATE/questions.yml similarity index 88% rename from .github/ISSUE_TEMPLATE/question.yml rename to .github/DISCUSSION_TEMPLATE/questions.yml index a57446755e..524d5c2d08 100644 --- a/.github/ISSUE_TEMPLATE/question.yml +++ b/.github/DISCUSSION_TEMPLATE/questions.yml @@ -1,5 +1,3 @@ -name: Question or Problem -description: Ask a question or ask about a problem labels: [question] body: - type: markdown @@ -8,29 +6,29 @@ body: Thanks for your interest in SQLModel! 🚀 Please follow these instructions, fill every question, and do every step. 🙏 - - I'm asking this because answering questions and solving problems in GitHub issues is what consumes most of the time. - - I end up not being able to add new features, fix bugs, review pull requests, etc. as fast as I wish because I have to spend too much time handling issues. + + I'm asking this because answering questions and solving problems in GitHub is what consumes most of the time. + + I end up not being able to add new features, fix bugs, review pull requests, etc. as fast as I wish because I have to spend too much time handling questions. All that, on top of all the incredible help provided by a bunch of community members that give a lot of their time to come here and help others. If more SQLModel users came to help others like them just a little bit more, it would be much less effort for them (and you and me 😅). By asking questions in a structured way (following this) it will be much easier to help you. - + And there's a high chance that you will find the solution along the way and you won't even have to submit it and wait for an answer. 😎 - As there are too many issues with questions, I'll have to close the incomplete ones. That will allow me (and others) to focus on helping people like you that follow the whole process and help us help you. 🤓 + As there are too many questions, I'll have to discard and close the incomplete ones. That will allow me (and others) to focus on helping people like you that follow the whole process and help us help you. 🤓 - type: checkboxes id: checks attributes: label: First Check description: Please confirm and check all the following options. options: - - label: I added a very descriptive title to this issue. + - label: I added a very descriptive title here. required: true - - label: I used the GitHub search to find a similar issue and didn't find it. + - label: I used the GitHub search to find a similar question and didn't find it. required: true - label: I searched the SQLModel documentation, with the integrated search. required: true @@ -48,10 +46,10 @@ body: label: Commit to Help description: | After submitting this, I commit to one of: - + * Read open issues with questions until I find 2 issues where I can help someone and add a comment to help there. * I already hit the "watch" button in this repository to receive notifications and I commit to help at least 2 people that ask questions in the future. - * Implement a Pull Request for a confirmed bug. + * Review one Pull Request by downloading the code and following all the review process](https://sqlmodel.tiangolo.com/help/#review-pull-requests). options: - label: I commit to help with one of those options 👆 @@ -66,16 +64,14 @@ body: If I (or someone) can copy it, run it, and see it right away, there's a much higher chance I (or someone) will be able to help you. placeholder: | - from typing import Optional - from sqlmodel import Field, Session, SQLModel, create_engine class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) + id: int | None = Field(default=None, primary_key=True) name: str secret_name: str - age: Optional[int] = None + age: int | None = None hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 55749398fd..b4f46138cd 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -2,3 +2,12 @@ blank_issues_enabled: false contact_links: - name: Security Contact about: Please report security vulnerabilities to security@tiangolo.com + - name: Question or Problem + about: Ask a question or ask about a problem in GitHub Discussions. + url: https://github.com/fastapi/sqlmodel/discussions/categories/questions + - name: Feature Request + about: To suggest an idea or ask about a feature, please start with a question saying what you would like to achieve. There might be a way to do it already. + url: https://github.com/fastapi/sqlmodel/discussions/categories/questions + - name: Show and tell + about: Show what you built with SQLModel or to be used with SQLModel. + url: https://github.com/fastapi/sqlmodel/discussions/categories/show-and-tell diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml deleted file mode 100644 index 5f18e8d420..0000000000 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ /dev/null @@ -1,214 +0,0 @@ -name: Feature Request -description: Suggest an idea or ask for a feature that you would like to have in SQLModel -labels: [enhancement] -body: - - type: markdown - attributes: - value: | - Thanks for your interest in SQLModel! 🚀 - - Please follow these instructions, fill every question, and do every step. 🙏 - - I'm asking this because answering questions and solving problems in GitHub issues is what consumes most of the time. - - I end up not being able to add new features, fix bugs, review pull requests, etc. as fast as I wish because I have to spend too much time handling issues. - - All that, on top of all the incredible help provided by a bunch of community members that give a lot of their time to come here and help others. - - If more SQLModel users came to help others like them just a little bit more, it would be much less effort for them (and you and me 😅). - - By asking questions in a structured way (following this) it will be much easier to help you. - - And there's a high chance that you will find the solution along the way and you won't even have to submit it and wait for an answer. 😎 - - As there are too many issues with questions, I'll have to close the incomplete ones. That will allow me (and others) to focus on helping people like you that follow the whole process and help us help you. 🤓 - - type: checkboxes - id: checks - attributes: - label: First Check - description: Please confirm and check all the following options. - options: - - label: I added a very descriptive title to this issue. - required: true - - label: I used the GitHub search to find a similar issue and didn't find it. - required: true - - label: I searched the SQLModel documentation, with the integrated search. - required: true - - label: I already searched in Google "How to X in SQLModel" and didn't find any information. - required: true - - label: I already read and followed all the tutorial in the docs and didn't find an answer. - required: true - - label: I already checked if it is not related to SQLModel but to [Pydantic](https://github.com/samuelcolvin/pydantic). - required: true - - label: I already checked if it is not related to SQLModel but to [SQLAlchemy](https://github.com/sqlalchemy/sqlalchemy). - required: true - - type: checkboxes - id: help - attributes: - label: Commit to Help - description: | - After submitting this, I commit to one of: - - * Read open issues with questions until I find 2 issues where I can help someone and add a comment to help there. - * I already hit the "watch" button in this repository to receive notifications and I commit to help at least 2 people that ask questions in the future. - * Implement a Pull Request for a confirmed bug. - - options: - - label: I commit to help with one of those options 👆 - required: true - - type: textarea - id: example - attributes: - label: Example Code - description: | - Please add a self-contained, [minimal, reproducible, example](https://stackoverflow.com/help/minimal-reproducible-example) with your use case. - - If I (or someone) can copy it, run it, and see it right away, there's a much higher chance I (or someone) will be able to help you. - - placeholder: | - from typing import Optional - - from sqlmodel import Field, Session, SQLModel, create_engine - - - class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str - secret_name: str - age: Optional[int] = None - - - hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") - - engine = create_engine("sqlite:///database.db") - - - SQLModel.metadata.create_all(engine) - - with Session(engine) as session: - session.add(hero_1) - session.commit() - session.refresh(hero_1) - print(hero_1) - render: python - validations: - required: true - - type: textarea - id: description - attributes: - label: Description - description: | - What is your feature request? - - Write a short description telling me what you are trying to solve and what you are currently doing. - placeholder: | - * Create a Hero model. - * Create a Hero instance. - * Save it to a SQLite database. - * I would like it to also automatically send me an email with all the SQL code executed. - validations: - required: true - - type: textarea - id: wanted-solution - attributes: - label: Wanted Solution - description: | - Tell me what's the solution you would like. - placeholder: | - I would like it to have a `send_email` configuration that defaults to `False`, and can be set to `True` to send me an email. - validations: - required: true - - type: textarea - id: wanted-code - attributes: - label: Wanted Code - description: Show me an example of how you would want the code to look like. - placeholder: | - from typing import Optional - - from sqlmodel import Field, Session, SQLModel, create_engine - - - class Hero(SQLModel, table=True, send_email=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str - secret_name: str - age: Optional[int] = None - - - hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") - - engine = create_engine("sqlite:///database.db") - - - SQLModel.metadata.create_all(engine) - - with Session(engine) as session: - session.add(hero_1) - session.commit() - session.refresh(hero_1) - print(hero_1) - - - render: python - validations: - required: true - - type: textarea - id: alternatives - attributes: - label: Alternatives - description: | - Tell me about alternatives you've considered. - placeholder: | - To hire someone to look at the logs, write the SQL in paper, and then send me a letter by post with it. - - type: dropdown - id: os - attributes: - label: Operating System - description: What operating system are you on? - multiple: true - options: - - Linux - - Windows - - macOS - - Other - validations: - required: true - - type: textarea - id: os-details - attributes: - label: Operating System Details - description: You can add more details about your operating system here, in particular if you chose "Other". - - type: input - id: sqlmodel-version - attributes: - label: SQLModel Version - description: | - What SQLModel version are you using? - - You can find the SQLModel version with: - - ```bash - python -c "import sqlmodel; print(sqlmodel.__version__)" - ``` - validations: - required: true - - type: input - id: python-version - attributes: - label: Python Version - description: | - What Python version are you using? - - You can find the Python version with: - - ```bash - python --version - ``` - validations: - required: true - - type: textarea - id: context - attributes: - label: Additional Context - description: Add any additional context information or screenshots you think are useful. diff --git a/.github/ISSUE_TEMPLATE/privileged.yml b/.github/ISSUE_TEMPLATE/privileged.yml new file mode 100644 index 0000000000..8cc6002633 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/privileged.yml @@ -0,0 +1,22 @@ +name: Privileged +description: You are @tiangolo or he asked you directly to create an issue here. If not, check the other options. 👇 +body: + - type: markdown + attributes: + value: | + Thanks for your interest in SQLModel! 🚀 + + If you are not @tiangolo or he didn't ask you directly to create an issue here, please start the conversation in a [Question in GitHub Discussions](https://github.com/fastapi/sqlmodel/discussions/categories/questions) instead. + - type: checkboxes + id: privileged + attributes: + label: Privileged issue + description: Confirm that you are allowed to create an issue here. + options: + - label: I'm @tiangolo or he asked me directly to create an issue here. + required: true + - type: textarea + id: content + attributes: + label: Issue Content + description: Add the content of the issue here. diff --git a/.github/actions/comment-docs-preview-in-pr/Dockerfile b/.github/actions/comment-docs-preview-in-pr/Dockerfile deleted file mode 100644 index 4f20c5f10b..0000000000 --- a/.github/actions/comment-docs-preview-in-pr/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM python:3.7 - -RUN pip install httpx "pydantic==1.5.1" pygithub - -COPY ./app /app - -CMD ["python", "/app/main.py"] diff --git a/.github/actions/comment-docs-preview-in-pr/action.yml b/.github/actions/comment-docs-preview-in-pr/action.yml deleted file mode 100644 index 0eb64402d2..0000000000 --- a/.github/actions/comment-docs-preview-in-pr/action.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: Comment Docs Preview in PR -description: Comment with the docs URL preview in the PR -author: Sebastián Ramírez -inputs: - token: - description: Token for the repo. Can be passed in using {{ secrets.GITHUB_TOKEN }} - required: true - deploy_url: - description: The deployment URL to comment in the PR - required: true -runs: - using: docker - image: Dockerfile diff --git a/.github/actions/comment-docs-preview-in-pr/app/main.py b/.github/actions/comment-docs-preview-in-pr/app/main.py deleted file mode 100644 index 3b10e0ee08..0000000000 --- a/.github/actions/comment-docs-preview-in-pr/app/main.py +++ /dev/null @@ -1,70 +0,0 @@ -import logging -import sys -from pathlib import Path -from typing import Optional - -import httpx -from github import Github -from github.PullRequest import PullRequest -from pydantic import BaseModel, BaseSettings, SecretStr, ValidationError - -github_api = "https://api.github.com" - - -class Settings(BaseSettings): - github_repository: str - github_event_path: Path - github_event_name: Optional[str] = None - input_token: SecretStr - input_deploy_url: str - - -class PartialGithubEventHeadCommit(BaseModel): - id: str - - -class PartialGithubEventWorkflowRun(BaseModel): - head_commit: PartialGithubEventHeadCommit - - -class PartialGithubEvent(BaseModel): - workflow_run: PartialGithubEventWorkflowRun - - -if __name__ == "__main__": - logging.basicConfig(level=logging.INFO) - settings = Settings() - logging.info(f"Using config: {settings.json()}") - g = Github(settings.input_token.get_secret_value()) - repo = g.get_repo(settings.github_repository) - try: - event = PartialGithubEvent.parse_file(settings.github_event_path) - except ValidationError as e: - logging.error(f"Error parsing event file: {e.errors()}") - sys.exit(0) - use_pr: Optional[PullRequest] = None - for pr in repo.get_pulls(): - if pr.head.sha == event.workflow_run.head_commit.id: - use_pr = pr - break - if not use_pr: - logging.error( - f"No PR found for hash: {event.workflow_run.head_commit.id}" - ) - sys.exit(0) - github_headers = { - "Authorization": f"token {settings.input_token.get_secret_value()}" - } - url = f"{github_api}/repos/{settings.github_repository}/issues/{use_pr.number}/comments" - logging.info(f"Using comments URL: {url}") - response = httpx.post( - url, - headers=github_headers, - json={ - "body": f"📝 Docs preview for commit {use_pr.head.sha} at: {settings.input_deploy_url}" - }, - ) - if not (200 <= response.status_code <= 300): - logging.error(f"Error posting comment: {response.text}") - sys.exit(1) - logging.info("Finished") diff --git a/.github/actions/watch-previews/Dockerfile b/.github/actions/watch-previews/Dockerfile deleted file mode 100644 index b8cc64d948..0000000000 --- a/.github/actions/watch-previews/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM python:3.7 - -RUN pip install httpx PyGithub "pydantic==1.5.1" - -COPY ./app /app - -CMD ["python", "/app/main.py"] diff --git a/.github/actions/watch-previews/action.yml b/.github/actions/watch-previews/action.yml deleted file mode 100644 index a9c4b3f78a..0000000000 --- a/.github/actions/watch-previews/action.yml +++ /dev/null @@ -1,10 +0,0 @@ -name: Watch docs previews in PRs -description: Check PRs and trigger new docs deploys -author: "Sebastián Ramírez " -inputs: - token: - description: 'Token for the repo. Can be passed in using {{ secrets.GITHUB_TOKEN }}' - required: true -runs: - using: docker - image: Dockerfile diff --git a/.github/actions/watch-previews/app/main.py b/.github/actions/watch-previews/app/main.py deleted file mode 100644 index 8a6d4a2525..0000000000 --- a/.github/actions/watch-previews/app/main.py +++ /dev/null @@ -1,102 +0,0 @@ -import logging -from datetime import datetime -from pathlib import Path -from typing import List, Optional - -import httpx -from github import Github -from github.NamedUser import NamedUser -from pydantic import BaseModel, BaseSettings, SecretStr - -github_api = "https://api.github.com" -netlify_api = "https://api.netlify.com" -main_branch = "main" - - -class Settings(BaseSettings): - input_token: SecretStr - github_repository: str - github_event_path: Path - github_event_name: Optional[str] = None - - -class Artifact(BaseModel): - id: int - node_id: str - name: str - size_in_bytes: int - url: str - archive_download_url: str - expired: bool - created_at: datetime - updated_at: datetime - - -class ArtifactResponse(BaseModel): - total_count: int - artifacts: List[Artifact] - - -def get_message(commit: str) -> str: - return f"Docs preview for commit {commit} at" - - -if __name__ == "__main__": - logging.basicConfig(level=logging.INFO) - settings = Settings() - logging.info(f"Using config: {settings.json()}") - g = Github(settings.input_token.get_secret_value()) - repo = g.get_repo(settings.github_repository) - owner: NamedUser = repo.owner - headers = {"Authorization": f"token {settings.input_token.get_secret_value()}"} - prs = list(repo.get_pulls(state="open")) - response = httpx.get( - f"{github_api}/repos/{settings.github_repository}/actions/artifacts", - headers=headers, - ) - data = response.json() - artifacts_response = ArtifactResponse.parse_obj(data) - for pr in prs: - logging.info("-----") - logging.info(f"Processing PR #{pr.number}: {pr.title}") - pr_comments = list(pr.get_issue_comments()) - pr_commits = list(pr.get_commits()) - last_commit = pr_commits[0] - for pr_commit in pr_commits: - if pr_commit.commit.author.date > last_commit.commit.author.date: - last_commit = pr_commit - commit = last_commit.commit.sha - logging.info(f"Last commit: {commit}") - message = get_message(commit) - notified = False - for pr_comment in pr_comments: - if message in pr_comment.body: - notified = True - logging.info(f"Docs preview was notified: {notified}") - if not notified: - artifact_name = f"docs-zip-{commit}" - use_artifact: Optional[Artifact] = None - for artifact in artifacts_response.artifacts: - if artifact.name == artifact_name: - use_artifact = artifact - break - if not use_artifact: - logging.info("Artifact not available") - else: - logging.info(f"Existing artifact: {use_artifact.name}") - response = httpx.post( - f"{github_api}/repos/{settings.github_repository}/actions/workflows/preview-docs.yml/dispatches", - headers=headers, - json={ - "ref": main_branch, - "inputs": { - "pr": f"{pr.number}", - "name": artifact_name, - "commit": commit, - }, - }, - ) - logging.info( - f"Trigger sent, response status: {response.status_code} - content: {response.content}" - ) - logging.info("Finished") diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b38df29f46..cd972a0ba4 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,6 +1,16 @@ version: 2 updates: + # GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + commit-message: + prefix: ⬆ + # Python - package-ecosystem: "pip" directory: "/" schedule: interval: "daily" + commit-message: + prefix: ⬆ diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000000..1af3d5e408 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,24 @@ +docs: + - all: + - changed-files: + - any-glob-to-any-file: + - docs/** + - docs_src/** + - all-globs-to-all-files: + - '!sqlmodel/**' + - '!pyproject.toml' + +internal: + - all: + - changed-files: + - any-glob-to-any-file: + - .github/** + - scripts/** + - .gitignore + - .pre-commit-config.yaml + - pdm_build.py + - requirements*.txt + - all-globs-to-all-files: + - '!docs/**' + - '!sqlmodel/**' + - '!pyproject.toml' diff --git a/.github/workflows/add-to-project.yml b/.github/workflows/add-to-project.yml new file mode 100644 index 0000000000..dccea83f35 --- /dev/null +++ b/.github/workflows/add-to-project.yml @@ -0,0 +1,18 @@ +name: Add to Project + +on: + pull_request_target: + issues: + types: + - opened + - reopened + +jobs: + add-to-project: + name: Add to project + runs-on: ubuntu-latest + steps: + - uses: actions/add-to-project@v1.0.2 + with: + project-url: https://github.com/orgs/fastapi/projects/2 + github-token: ${{ secrets.PROJECTS_TOKEN }} diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 18e35b308e..29767ef998 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -4,78 +4,90 @@ on: branches: - main pull_request: - types: [opened, synchronize] - workflow_dispatch: - inputs: - debug_enabled: - description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' - required: false - default: false + types: + - opened + - synchronize + +env: + UV_SYSTEM_PYTHON: 1 + + jobs: + changes: + runs-on: ubuntu-latest + # Required permissions + permissions: + pull-requests: read + # Set job outputs to values from filter step + outputs: + docs: ${{ steps.filter.outputs.docs }} + steps: + - uses: actions/checkout@v6 + # For pull requests it's not necessary to checkout the code but for the main branch it is + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + docs: + - README.md + - docs/** + - docs_src/** + - requirements-docs.txt + - pyproject.toml + - mkdocs.yml + - mkdocs.env.yml + - .github/workflows/build-docs.yml + - .github/workflows/deploy-docs.yml + - data/** + build-docs: - runs-on: ubuntu-20.04 + needs: + - changes + if: ${{ needs.changes.outputs.docs == 'true' }} + runs-on: ubuntu-latest steps: - name: Dump GitHub context env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v2 + - uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: "3.7" - # Allow debugging with tmate - - name: Setup tmate session - uses: mxschmitt/action-tmate@v3 - if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }} + uses: actions/setup-python@v6 with: - limit-access-to-actor: true - - uses: actions/cache@v2 - id: cache + python-version: "3.11" + - name: Setup uv + uses: astral-sh/setup-uv@v7 with: - path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-root-docs - - name: Install poetry - if: steps.cache.outputs.cache-hit != 'true' - # TODO: remove python -m pip install --force git+https://github.com/python-poetry/poetry-core.git@ad33bc2 - # once there's a release of Poetry 1.2.x including poetry-core > 1.1.0a6 - # Ref: https://github.com/python-poetry/poetry-core/pull/188 - run: | - python -m pip install --upgrade pip - python -m pip install --force git+https://github.com/python-poetry/poetry-core.git@ad33bc2 - python -m pip install "poetry==1.2.0a2" - python -m poetry plugin add poetry-version-plugin - - name: Configure poetry - run: python -m poetry config virtualenvs.create false - - name: Install Dependencies - if: steps.cache.outputs.cache-hit != 'true' - run: python -m poetry install - - name: Install Material for MkDocs Insiders - if: github.event.pull_request.head.repo.fork == false && steps.cache.outputs.cache-hit != 'true' - run: python -m poetry run pip install git+https://${{ secrets.ACTIONS_TOKEN }}@github.com/squidfunk/mkdocs-material-insiders.git - - uses: actions/cache@v2 + version: "0.4.15" + enable-cache: true + cache-dependency-glob: | + requirements**.txt + pyproject.toml + - name: Install docs extras + run: uv pip install -r requirements-docs.txt + - uses: actions/cache@v5 with: key: mkdocs-cards-${{ github.ref }} path: .cache + - name: Verify README + run: python ./scripts/docs.py verify-readme - name: Build Docs - if: github.event.pull_request.head.repo.fork == true - run: python -m poetry run mkdocs build - - name: Build Docs with Insiders - if: github.event.pull_request.head.repo.fork == false - run: python -m poetry run mkdocs build --config-file mkdocs.insiders.yml - - name: Zip docs - run: python -m poetry run bash ./scripts/zip-docs.sh - - uses: actions/upload-artifact@v2 + run: python ./scripts/docs.py build + - uses: actions/upload-artifact@v6 with: - name: docs-zip - path: ./docs.zip - - name: Deploy to Netlify - uses: nwtgck/actions-netlify@v1.1.5 + name: docs-site + path: ./site/** + include-hidden-files: true + + # https://github.com/marketplace/actions/alls-green#why + docs-all-green: # This job does nothing and is only used for the branch protection + if: always() + needs: + - build-docs + runs-on: ubuntu-latest + steps: + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@release/v1 with: - publish-dir: './site' - production-branch: main - github-token: ${{ secrets.GITHUB_TOKEN }} - enable-commit-comment: false - env: - NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} - NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} + jobs: ${{ toJSON(needs) }} + allowed-skips: build-docs diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 0000000000..519e4ae667 --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,86 @@ +name: Deploy Docs +on: + workflow_run: + workflows: + - Build Docs + types: + - completed + +permissions: + deployments: write + issues: write + pull-requests: write + statuses: write + +env: + UV_SYSTEM_PYTHON: 1 + +jobs: + deploy-docs: + runs-on: ubuntu-latest + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: actions/checkout@v6 + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.11" + - name: Setup uv + uses: astral-sh/setup-uv@v7 + with: + version: "0.4.15" + enable-cache: true + cache-dependency-glob: | + requirements**.txt + pyproject.toml + - name: Install GitHub Actions dependencies + run: uv pip install -r requirements-github-actions.txt + - name: Deploy Docs Status Pending + run: python ./scripts/deploy_docs_status.py + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COMMIT_SHA: ${{ github.event.workflow_run.head_sha }} + RUN_ID: ${{ github.run_id }} + STATE: "pending" + - name: Clean site + run: | + rm -rf ./site + mkdir ./site + - uses: actions/download-artifact@v7 + with: + path: ./site/ + pattern: docs-site + merge-multiple: true + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} + - name: Deploy to Cloudflare Pages + # hashFiles returns an empty string if there are no files + if: hashFiles('./site/*') + id: deploy + env: + PROJECT_NAME: sqlmodel + BRANCH: ${{ ( github.event.workflow_run.head_repository.full_name == github.repository && github.event.workflow_run.head_branch == 'main' && 'main' ) || ( github.event.workflow_run.head_sha ) }} + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: pages deploy ./site --project-name=${{ env.PROJECT_NAME }} --branch=${{ env.BRANCH }} + - name: Deploy Docs Status Error + if: failure() + run: python ./scripts/deploy_docs_status.py + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COMMIT_SHA: ${{ github.event.workflow_run.head_sha }} + RUN_ID: ${{ github.run_id }} + STATE: "error" + - name: Comment Deploy + run: python ./scripts/deploy_docs_status.py + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DEPLOY_URL: ${{ steps.deploy.outputs.deployment-url }} + COMMIT_SHA: ${{ github.event.workflow_run.head_sha }} + RUN_ID: ${{ github.run_id }} + STATE: "success" diff --git a/.github/workflows/detect-conflicts.yml b/.github/workflows/detect-conflicts.yml new file mode 100644 index 0000000000..aba329db85 --- /dev/null +++ b/.github/workflows/detect-conflicts.yml @@ -0,0 +1,19 @@ +name: "Conflict detector" +on: + push: + pull_request_target: + types: [synchronize] + +jobs: + main: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - name: Check if PRs have merge conflicts + uses: eps1lon/actions-label-merge-conflict@v3 + with: + dirtyLabel: "conflicts" + repoToken: "${{ secrets.GITHUB_TOKEN }}" + commentOnDirty: "This pull request has a merge conflict that needs to be resolved." diff --git a/.github/workflows/issue-manager.yml b/.github/workflows/issue-manager.yml index e2fb4f7a43..137ed16de3 100644 --- a/.github/workflows/issue-manager.yml +++ b/.github/workflows/issue-manager.yml @@ -2,7 +2,7 @@ name: Issue Manager on: schedule: - - cron: "0 0 * * *" + - cron: "13 18 * * *" issue_comment: types: - created @@ -14,11 +14,20 @@ on: - labeled workflow_dispatch: +permissions: + issues: write + pull-requests: write + jobs: issue-manager: + if: github.repository_owner == 'fastapi' runs-on: ubuntu-latest steps: - - uses: tiangolo/issue-manager@0.4.0 + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: tiangolo/issue-manager@0.6.0 with: token: ${{ secrets.GITHUB_TOKEN }} config: > @@ -26,5 +35,17 @@ jobs: "answered": { "delay": 864000, "message": "Assuming the original need was handled, this will be automatically closed now. But feel free to add more comments or create new issues or PRs." + }, + "waiting": { + "delay": 2628000, + "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." + } + }, + "invalid": { + "delay": 0, + "message": "This was marked as invalid and will be closed now. If this is an error, please provide additional details." } } diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 0000000000..7aeb448e6f --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,33 @@ +name: Labels +on: + pull_request_target: + types: + - opened + - synchronize + - reopened + # For label-checker + - labeled + - unlabeled + +jobs: + labeler: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@v6 + if: ${{ github.event.action != 'labeled' && github.event.action != 'unlabeled' }} + - run: echo "Done adding labels" + # Run this after labeler applied labels + check-labels: + needs: + - labeler + permissions: + pull-requests: read + runs-on: ubuntu-latest + steps: + - uses: docker://agilepathway/pull-request-label-checker:latest + with: + one_of: breaking,security,feature,bug,refactor,upgrade,docs,lang-all,internal + repo_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/latest-changes.yml b/.github/workflows/latest-changes.yml index 48fb6dc833..bdfaa4b2ad 100644 --- a/.github/workflows/latest-changes.yml +++ b/.github/workflows/latest-changes.yml @@ -12,27 +12,29 @@ on: description: PR number required: true debug_enabled: - description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' + description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' required: false - default: false + default: 'false' jobs: latest-changes: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v6 with: # To allow latest-changes to commit to the main branch - token: ${{ secrets.ACTIONS_TOKEN }} + token: ${{ secrets.SQLMODEL_LATEST_CHANGES }} # Allow debugging with tmate - name: Setup tmate session uses: mxschmitt/action-tmate@v3 - if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }} + if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} with: limit-access-to-actor: true - - uses: docker://tiangolo/latest-changes:0.0.3 + - uses: tiangolo/latest-changes@0.4.1 with: token: ${{ secrets.GITHUB_TOKEN }} latest_changes_file: docs/release-notes.md - latest_changes_header: '## Latest Changes\n\n' + latest_changes_header: '## Latest Changes' + end_regex: '^## ' debug_logs: true + label_header_prefix: '### ' diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000000..ff5340aeb7 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,93 @@ +name: pre-commit + +on: + pull_request: + types: + - opened + - synchronize + +env: + # Forks and Dependabot don't have access to secrets + HAS_SECRETS: ${{ secrets.PRE_COMMIT != '' }} + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: actions/checkout@v6 + name: Checkout PR for own repo + if: env.HAS_SECRETS == 'true' + with: + # To be able to commit it needs to fetch the head of the branch, not the + # merge commit + ref: ${{ github.head_ref }} + # And it needs the full history to be able to compute diffs + fetch-depth: 0 + # A token other than the default GITHUB_TOKEN is needed to be able to trigger CI + token: ${{ secrets.PRE_COMMIT }} + # pre-commit lite ci needs the default checkout configs to work + - uses: actions/checkout@v6 + name: Checkout PR for fork + if: env.HAS_SECRETS == 'false' + with: + # To be able to commit it needs the head branch of the PR, the remote one + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.14" + - name: Setup uv + uses: astral-sh/setup-uv@v7 + with: + cache-dependency-glob: | + requirements**.txt + pyproject.toml + uv.lock + - name: Install Dependencies + run: | + uv venv + uv pip install -r requirements.txt + - name: Run prek - pre-commit + id: precommit + run: uvx prek run --from-ref origin/${GITHUB_BASE_REF} --to-ref HEAD --show-diff-on-failure + continue-on-error: true + - name: Commit and push changes + if: env.HAS_SECRETS == 'true' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add -A + if git diff --staged --quiet; then + echo "No changes to commit" + else + git commit -m "🎨 Auto format" + git push + fi + - uses: pre-commit-ci/lite-action@v1.1.0 + if: env.HAS_SECRETS == 'false' + with: + msg: 🎨 Auto format + - name: Error out on pre-commit errors + if: steps.precommit.outcome == 'failure' + run: exit 1 + + # https://github.com/marketplace/actions/alls-green#why + pre-commit-alls-green: # This job does nothing and is only used for the branch protection + if: always() + needs: + - pre-commit + runs-on: ubuntu-latest + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/preview-docs.yml deleted file mode 100644 index e335e81f91..0000000000 --- a/.github/workflows/preview-docs.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Preview Docs -on: - workflow_run: - workflows: - - Build Docs - types: - - completed - -jobs: - preview-docs: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v2 - - name: Download Artifact Docs - uses: dawidd6/action-download-artifact@v2.9.0 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - workflow: build-docs.yml - run_id: ${{ github.event.workflow_run.id }} - name: docs-zip - - name: Unzip docs - run: | - rm -rf ./site - unzip docs.zip - rm -f docs.zip - - name: Deploy to Netlify - id: netlify - uses: nwtgck/actions-netlify@v1.1.5 - with: - publish-dir: './site' - production-deploy: false - github-token: ${{ secrets.GITHUB_TOKEN }} - enable-commit-comment: false - env: - NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} - NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} - - name: Comment Deploy - uses: ./.github/actions/comment-docs-preview-in-pr - with: - token: ${{ secrets.GITHUB_TOKEN }} - deploy_url: "${{ steps.netlify.outputs.deploy-url }}" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 105dbdd4cc..05543b2f15 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -7,48 +7,31 @@ on: workflow_dispatch: inputs: debug_enabled: - description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' + description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' required: false - default: false + default: 'false' jobs: publish: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest + strategy: + matrix: + package: + - sqlmodel + - sqlmodel-slim + permissions: + id-token: write steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v6 with: - python-version: "3.7" - # Allow debugging with tmate - - name: Setup tmate session - uses: mxschmitt/action-tmate@v3 - if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }} - with: - limit-access-to-actor: true - - uses: actions/cache@v2 - id: cache - with: - path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-root - - name: Install poetry - if: steps.cache.outputs.cache-hit != 'true' - # TODO: remove python -m pip install --force git+https://github.com/python-poetry/poetry-core.git@ad33bc2 - # once there's a release of Poetry 1.2.x including poetry-core > 1.1.0a6 - # Ref: https://github.com/python-poetry/poetry-core/pull/188 - run: | - python -m pip install --upgrade pip - python -m pip install --force git+https://github.com/python-poetry/poetry-core.git@ad33bc2 - python -m pip install "poetry==1.2.0a2" - python -m poetry plugin add poetry-version-plugin - - name: Configure poetry - run: python -m poetry config virtualenvs.create false - - name: Install Dependencies - if: steps.cache.outputs.cache-hit != 'true' - run: python -m poetry install - - name: Publish + python-version: "3.11" + - name: Install build dependencies + run: pip install build + - name: Build distribution env: - PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} - run: | - python -m poetry config pypi-token.pypi $PYPI_TOKEN - bash scripts/publish.sh + TIANGOLO_BUILD_PACKAGE: ${{ matrix.package }} + run: python -m build + - name: Publish + uses: pypa/gh-action-pypi-publish@v1.13.0 diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml new file mode 100644 index 0000000000..c4b72d4b65 --- /dev/null +++ b/.github/workflows/smokeshow.yml @@ -0,0 +1,52 @@ +name: Smokeshow + +on: + workflow_run: + workflows: [Test] + types: [completed] + +permissions: + statuses: write + +env: + UV_SYSTEM_PYTHON: 1 + +jobs: + smokeshow: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: '3.13' + - name: Setup uv + uses: astral-sh/setup-uv@v7 + with: + cache-dependency-glob: | + requirements**.txt + pyproject.toml + - run: uv pip install -r requirements-github-actions.txt + - uses: actions/download-artifact@v7 + with: + name: coverage-html + path: htmlcov + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} + # Try 5 times to upload coverage to smokeshow + - name: Upload coverage to Smokeshow + run: | + for i in 1 2 3 4 5; do + if smokeshow upload htmlcov; then + echo "Smokeshow upload success!" + break + fi + echo "Smokeshow upload error, sleep 1 sec and try again." + sleep 1 + done + env: + SMOKESHOW_GITHUB_STATUS_DESCRIPTION: Coverage {coverage-percentage} + SMOKESHOW_GITHUB_COVERAGE_THRESHOLD: 99 + SMOKESHOW_GITHUB_CONTEXT: coverage + SMOKESHOW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SMOKESHOW_GITHUB_PR_HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + SMOKESHOW_AUTH_KEY: ${{ secrets.SMOKESHOW_AUTH_KEY }} diff --git a/.github/workflows/test-redistribute.yml b/.github/workflows/test-redistribute.yml new file mode 100644 index 0000000000..1a51b367d6 --- /dev/null +++ b/.github/workflows/test-redistribute.yml @@ -0,0 +1,65 @@ +name: Test Redistribute + +on: + push: + branches: + - main + pull_request: + types: + - opened + - synchronize + +jobs: + test-redistribute: + runs-on: ubuntu-latest + strategy: + matrix: + package: + - sqlmodel + - sqlmodel-slim + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: actions/checkout@v6 + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.10" + - name: Install build dependencies + run: pip install build + - name: Build source distribution + env: + TIANGOLO_BUILD_PACKAGE: ${{ matrix.package }} + run: python -m build --sdist + - name: Decompress source distribution + run: | + cd dist + tar xvf sqlmodel*.tar.gz + - name: Install test dependencies + run: | + cd dist/sqlmodel*/ + pip install -r requirements-tests.txt + env: + TIANGOLO_BUILD_PACKAGE: ${{ matrix.package }} + - name: Run source distribution tests + run: | + cd dist/sqlmodel*/ + bash scripts/test.sh + - name: Build wheel distribution + run: | + cd dist + pip wheel --no-deps sqlmodel*.tar.gz + + # https://github.com/marketplace/actions/alls-green#why + test-redistribute-alls-green: # This job does nothing and is only used for the branch protection + if: always() + needs: + - test-redistribute + runs-on: ubuntu-latest + steps: + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 744a0fa250..106731cde4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,58 +5,122 @@ on: branches: - main pull_request: - types: [opened, synchronize] + types: + - opened + - synchronize workflow_dispatch: inputs: debug_enabled: - description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' + description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' required: false - default: false + default: 'false' + schedule: + # cron every week on monday + - cron: "0 0 * * 1" + +env: + UV_SYSTEM_PYTHON: 1 jobs: test: - runs-on: ubuntu-20.04 strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9] + os: [ ubuntu-latest, windows-latest, macos-latest ] + python-version: [ "3.14" ] + include: + - os: windows-latest + python-version: "3.9" + - os: ubuntu-latest + python-version: "3.10" + - os: macos-latest + python-version: "3.11" + - os: windows-latest + python-version: "3.12" + - os: ubuntu-latest + python-version: "3.13" + - os: macos-latest + python-version: "3.13" fail-fast: false - + runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} + - name: Setup uv + uses: astral-sh/setup-uv@v7 + with: + version: "0.4.15" + enable-cache: true + cache-dependency-glob: | + requirements**.txt + pyproject.toml # Allow debugging with tmate - name: Setup tmate session uses: mxschmitt/action-tmate@v3 - if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }} + if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} with: limit-access-to-actor: true - - uses: actions/cache@v2 - id: cache - with: - path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-root - - name: Install poetry - if: steps.cache.outputs.cache-hit != 'true' - # TODO: remove python -m pip install --force git+https://github.com/python-poetry/poetry-core.git@ad33bc2 - # once there's a release of Poetry 1.2.x including poetry-core > 1.1.0a6 - # Ref: https://github.com/python-poetry/poetry-core/pull/188 - run: | - python -m pip install --upgrade pip - python -m pip install --force git+https://github.com/python-poetry/poetry-core.git@ad33bc2 - python -m pip install "poetry==1.2.0a2" - python -m poetry plugin add poetry-version-plugin - - name: Configure poetry - run: python -m poetry config virtualenvs.create false - name: Install Dependencies - if: steps.cache.outputs.cache-hit != 'true' - run: python -m poetry install - - name: Lint - if: ${{ matrix.python-version != '3.6' }} - run: python -m poetry run bash scripts/lint.sh + run: uv pip install -r requirements-tests.txt + - run: mkdir coverage - name: Test - run: python -m poetry run bash scripts/test.sh - - name: Upload coverage - uses: codecov/codecov-action@v1 + run: bash scripts/test.sh + env: + COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}-${{ matrix.pydantic-version }} + CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }} + - name: Store coverage files + uses: actions/upload-artifact@v6 + with: + name: coverage-${{ runner.os }}-${{ matrix.python-version }}-${{ matrix.pydantic-version }} + path: coverage + include-hidden-files: true + + coverage-combine: + needs: + - test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: '3.13' + - name: Setup uv + uses: astral-sh/setup-uv@v7 + with: + version: "0.4.15" + enable-cache: true + cache-dependency-glob: | + requirements**.txt + pyproject.toml + - name: Get coverage files + uses: actions/download-artifact@v7 + with: + pattern: coverage-* + path: coverage + merge-multiple: true + - name: Install Dependencies + run: uv pip install -r requirements-tests.txt + - run: ls -la coverage + - run: coverage combine coverage + - run: coverage html --title "Coverage for ${{ github.sha }}" + - name: Store coverage HTML + uses: actions/upload-artifact@v6 + with: + name: coverage-html + path: htmlcov + include-hidden-files: true + - run: coverage report --fail-under=99 + + # https://github.com/marketplace/actions/alls-green#why + alls-green: # This job does nothing and is only used for the branch protection + if: always() + needs: + - coverage-combine + runs-on: ubuntu-latest + steps: + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} diff --git a/.gitignore b/.gitignore index 4006069389..9e195bfa79 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ *.pyc -env* .mypy_cache .vscode .idea @@ -7,8 +6,9 @@ poetry.lock dist htmlcov *.egg-info -.coverage +.coverage* coverage.xml site *.db .cache +.venv* diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000..41c64c030c --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,35 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-added-large-files + - id: check-toml + - id: check-yaml + args: + - --unsafe + - id: end-of-file-fixer + - id: trailing-whitespace + + - repo: local + hooks: + - id: local-ruff-check + name: ruff check + entry: uv run ruff check --force-exclude --fix --exit-non-zero-on-fix + require_serial: true + language: unsupported + types: [python] + + - id: local-ruff-format + name: ruff format + entry: uv run ruff format --force-exclude --exit-non-zero-on-format + require_serial: true + language: unsupported + types: [python] + + - id: generate-select + language: unsupported + name: generate-select + entry: uv run ./scripts/generate_select.py + files: ^scripts/generate_select\.py|sqlmodel/sql/_expression_select_gen\.py\.jinja2$ diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000000..5ea2d61946 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,24 @@ +# This CITATION.cff file was generated with cffinit. +# Visit https://bit.ly/cffinit to generate yours today! + +cff-version: 1.2.0 +title: SQLModel +message: >- + If you use this software, please cite it using the + metadata from this file. +type: software +authors: + - given-names: Sebastián + family-names: Ramírez + email: tiangolo@gmail.com +identifiers: +repository-code: 'https://github.com/fastapi/sqlmodel' +url: 'https://sqlmodel.tiangolo.com' +abstract: >- + SQLModel, SQL databases in Python, designed for + simplicity, compatibility, and robustness. +keywords: + - fastapi + - pydantic + - sqlalchemy +license: MIT diff --git a/README.md b/README.md index 5a63c9da44..712167ffd6 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,19 @@

- SQLModel + SQLModel +

SQLModel, SQL databases in Python, designed for simplicity, compatibility, and robustness.

- - Test - - - Publish + + Test - - Coverage + + Publish + + Coverage Package version @@ -23,7 +23,7 @@ **Documentation**: https://sqlmodel.tiangolo.com -**Source Code**: https://github.com/tiangolo/sqlmodel +**Source Code**: https://github.com/fastapi/sqlmodel --- @@ -39,6 +39,14 @@ The key features are: * **Extensible**: You have all the power of SQLAlchemy and Pydantic underneath. * **Short**: Minimize code duplication. A single type annotation does a lot of work. No need to duplicate models in SQLAlchemy and Pydantic. +## Sponsors + + + + + + + ## SQL Databases in FastAPI @@ -51,12 +59,14 @@ It combines SQLAlchemy and Pydantic and tries to simplify the code you write as ## Requirements -A recent and currently supported version of Python (right now, Python supports versions 3.6 and above). +A recent and currently supported version of Python. As **SQLModel** is based on **Pydantic** and **SQLAlchemy**, it requires them. They will be automatically installed when you install SQLModel. ## Installation +Make sure you create a virtual environment, activate it, and then install SQLModel, for example with: +

```console @@ -69,7 +79,7 @@ Successfully installed sqlmodel ## Example -For an introduction to databases, SQL, and everything else, see the SQLModel documentation. +For an introduction to databases, SQL, and everything else, see the SQLModel documentation. Here's a quick example. ✨ @@ -95,16 +105,14 @@ And you want it to have this data: Then you could create a **SQLModel** model like this: ```Python -from typing import Optional - from sqlmodel import Field, SQLModel class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) + id: int | None = Field(default=None, primary_key=True) name: str secret_name: str - age: Optional[int] = None + age: int | None = None ``` That class `Hero` is a **SQLModel** model, the equivalent of a SQL table in Python code. @@ -139,17 +147,15 @@ And **inline errors**: You can learn a lot more about **SQLModel** by quickly following the **tutorial**, but if you need a taste right now of how to put all that together and save to the database, you can do this: -```Python hl_lines="18 21 23-27" -from typing import Optional - +```Python hl_lines="16 19 21-25" from sqlmodel import Field, Session, SQLModel, create_engine class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) + id: int | None = Field(default=None, primary_key=True) name: str secret_name: str - age: Optional[int] = None + age: int | None = None hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") @@ -175,17 +181,15 @@ That will save a **SQLite** database with the 3 heroes. Then you could write queries to select from that same database, for example with: -```Python hl_lines="15-18" -from typing import Optional - +```Python hl_lines="13-17" from sqlmodel import Field, Session, SQLModel, create_engine, select class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) + id: int | None = Field(default=None, primary_key=True) name: str secret_name: str - age: Optional[int] = None + age: int | None = None engine = create_engine("sqlite:///database.db") @@ -212,4 +216,4 @@ And at the same time, ✨ it is also a **Pydantic** model ✨. You can use inher ## License -This project is licensed under the terms of the [MIT license](https://github.com/tiangolo/sqlmodel/blob/main/LICENSE). +This project is licensed under the terms of the [MIT license](https://github.com/fastapi/sqlmodel/blob/main/LICENSE). diff --git a/data/members.yml b/data/members.yml new file mode 100644 index 0000000000..7a51cbc9ac --- /dev/null +++ b/data/members.yml @@ -0,0 +1,3 @@ +members: +- login: tiangolo +- login: alejsdev diff --git a/data/sponsors.yml b/data/sponsors.yml new file mode 100644 index 0000000000..95cf878530 --- /dev/null +++ b/data/sponsors.yml @@ -0,0 +1,6 @@ +gold: [] +silver: + - url: https://www.govcert.lu + title: This project is being supported by GOVCERT.LU + img: https://sqlmodel.tiangolo.com/img/sponsors/govcert.png +bronze: [] diff --git a/docs/about/index.md b/docs/about/index.md new file mode 100644 index 0000000000..53cae9270d --- /dev/null +++ b/docs/about/index.md @@ -0,0 +1,3 @@ +# About + +About **SQLModel**, its design, inspiration, and more. 🤓 diff --git a/docs/advanced/decimal.md b/docs/advanced/decimal.md index c0541b75df..ce971b201b 100644 --- a/docs/advanced/decimal.md +++ b/docs/advanced/decimal.md @@ -19,36 +19,21 @@ In most cases this would probably not be a problem, for example measuring views ## Decimal Types -Pydantic has special support for `Decimal` types using the `condecimal()` special function. +Pydantic has special support for `Decimal` types. -!!! tip - Pydantic 1.9, that will be released soon, has improved support for `Decimal` types, without needing to use the `condecimal()` function. +When you use `Decimal` you can specify the number of digits and decimal places to support in the `Field()` function. They will be validated by Pydantic (for example when using FastAPI) and the same information will also be used for the database columns. - But meanwhile, you can already use this feature with `condecimal()` in **SQLModel** it as it's explained here. +/// info -When you use `condecimal()` you can specify the number of digits and decimal places to support. They will be validated by Pydantic (for example when using FastAPI) and the same information will also be used for the database columns. +For the database, **SQLModel** will use SQLAlchemy's `DECIMAL` type. -!!! info - For the database, **SQLModel** will use SQLAlchemy's `DECIMAL` type. +/// ## Decimals in SQLModel Let's say that each hero in the database will have an amount of money. We could make that field a `Decimal` type using the `condecimal()` function: -```{.python .annotate hl_lines="12" } -{!./docs_src/advanced/decimal/tutorial001.py[ln:1-12]!} - -# More code here later 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/advanced/decimal/tutorial001.py!} -``` - -
+{* ./docs_src/advanced/decimal/tutorial001_py310.py ln[1:11] hl[11] *} Here we are saying that `money` can have at most `5` digits with `max_digits`, **this includes the integers** (to the left of the decimal dot) **and the decimals** (to the right of the decimal dot). @@ -66,56 +51,29 @@ We are also saying that the number of decimal places (to the right of the decima 🚫 But these are all invalid numbers for that `money` field: * `1.2345` - * This number has more than 3 decimal places. + * This number has more than 3 decimal places. * `123.234` - * This number has more than 5 digits in total (integer and decimal part). + * This number has more than 5 digits in total (integer and decimal part). * `123` - * Even though this number doesn't have any decimals, we still have 3 places saved for them, which means that we can **only use 2 places** for the **integer part**, and this number has 3 integer digits. So, the allowed number of integer digits is `max_digits` - `decimal_places` = 2. + * Even though this number doesn't have any decimals, we still have 3 places saved for them, which means that we can **only use 2 places** for the **integer part**, and this number has 3 integer digits. So, the allowed number of integer digits is `max_digits` - `decimal_places` = 2. -!!! tip - Make sure you adjust the number of digits and decimal places for your own needs, in your own application. 🤓 +/// tip -## Create models with Decimals - -When creating new models you can actually pass normal (`float`) numbers, Pydantic will automatically convert them to `Decimal` types, and **SQLModel** will store them as `Decimal` types in the database (using SQLAlchemy). - -```Python hl_lines="4-6" -# Code above omitted 👆 +Make sure you adjust the number of digits and decimal places for your own needs, in your own application. 🤓 -{!./docs_src/advanced/decimal/tutorial001.py[ln:25-35]!} +/// -# Code below omitted 👇 -``` - -
-👀 Full file preview +## Create models with Decimals -```Python -{!./docs_src/advanced/decimal/tutorial001.py!} -``` +When creating new models you can actually pass normal (`float`) numbers, Pydantic will automatically convert them to `Decimal` types, and **SQLModel** will store them as `Decimal` types in the database (using SQLAlchemy). -
+{* ./docs_src/advanced/decimal/tutorial001_py310.py ln[24:34] hl[25:27] *} ## Select Decimal data Then, when working with Decimal types, you can confirm that they indeed avoid those rounding errors from floats: -```Python hl_lines="15-16" -# Code above omitted 👆 - -{!./docs_src/advanced/decimal/tutorial001.py[ln:38-51]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/advanced/decimal/tutorial001.py!} -``` - -
+{* ./docs_src/advanced/decimal/tutorial001_py310.py ln[37:50] hl[49:50] *} ## Review the results @@ -142,7 +100,10 @@ Total money: 3.300
-!!! warning - Although Decimal types are supported and used in the Python side, not all databases support it. In particular, SQLite doesn't support decimals, so it will convert them to the same floating `NUMERIC` type it supports. +/// warning + +Although Decimal types are supported and used in the Python side, not all databases support it. In particular, SQLite doesn't support decimals, so it will convert them to the same floating `NUMERIC` type it supports. + +But decimals are supported by most of the other SQL databases. 🎉 - But decimals are supported by most of the other SQL databases. 🎉 +/// diff --git a/docs/advanced/uuid.md b/docs/advanced/uuid.md new file mode 100644 index 0000000000..abfcfeda2b --- /dev/null +++ b/docs/advanced/uuid.md @@ -0,0 +1,178 @@ +# UUID (Universally Unique Identifiers) + +We have discussed some data types like `str`, `int`, etc. + +There's another data type called `UUID` (Universally Unique Identifier). + +You might have seen **UUIDs**, for example in URLs. They look something like this: + +``` +4ff2dab7-bffe-414d-88a5-1826b9fea8df +``` + +UUIDs can be particularly useful as an alternative to auto-incrementing integers for **primary keys**. + +/// info + +Official support for UUIDs was added in SQLModel version `0.0.20`. + +/// + +## About UUIDs + +UUIDs are numbers with 128 bits, that is, 16 bytes. + +They are normally seen as 32 hexadecimal characters separated by dashes. + +There are several versions of UUID, some versions include the current time in the bytes, but **UUIDs version 4** are mainly random, the way they are generated makes them virtually **unique**. + +### Distributed UUIDs + +You could generate one UUID in one computer, and someone else could generate another UUID in another computer, and it would be almost **impossible** for both UUIDs to be the **same**. + +This means that you don't have to wait for the DB to generate the ID for you, you can **generate it in code before sending it to the database**, because you can be quite certain it will be unique. + +/// note | Technical Details + +Because the number of possible UUIDs is so large (2^128), the probability of generating the same UUID version 4 (the random ones) twice is very low. + +If you had 103 trillion version 4 UUIDs stored in the database, the probability of generating a duplicated new one is one in a billion. 🤓 + +/// + +For the same reason, if you decided to migrate your database, combine it with another database and mix records, etc. you would most probably be able to **just use the same UUIDs** you had originally. + +/// warning + +There's still a chance you could have a collision, but it's very low. In most cases you could assume you wouldn't have it, but it would be good to be prepared for it. + +/// + +### UUIDs Prevent Information Leakage + +Because UUIDs version 4 are **random**, you could give these IDs to the application users or to other systems, **without exposing information** about your application. + +When using **auto-incremented integers** for primary keys, you could implicitly expose information about your system. For example, someone could create a new hero, and by getting the hero ID `20` **they would know that you have 20 heroes** in your system (or even less, if some heroes were already deleted). + +### UUID Storage + +Because UUIDs are 16 bytes, they would **consume more space** in the database than a smaller auto-incremented integer (commonly 4 bytes). + +Depending on the database you use, UUIDs could have **better or worse performance**. If you are concerned about that, you should check the documentation for the specific database. + +SQLite doesn't have a specific UUID type, so it will store the UUID as a string. Other databases like Postgres have a specific UUID type which would result in better performance and space usage than strings. + +## Models with UUIDs + +To use UUIDs as primary keys we need to import `uuid`, which is part of the Python standard library (we don't have to install anything) and use `uuid.UUID` as the **type** for the ID field. + +We also want the Python code to **generate a new UUID** when creating a new instance, so we use `default_factory`. + +The parameter `default_factory` takes a function (or in general, a "callable"). This function will be **called when creating a new instance** of the model and the value returned by the function will be used as the default value for the field. + +For the function in `default_factory` we pass `uuid.uuid4`, which is a function that generates a **new UUID version 4**. + +/// tip + +We don't call `uuid.uuid4()` ourselves in the code (we don't put the parenthesis). Instead, we pass the function itself, just `uuid.uuid4`, so that SQLModel can call it every time we create a new instance. + +/// + +This means that the UUID will be generated in the Python code, **before sending the data to the database**. + +{* ./docs_src/advanced/uuid/tutorial001_py310.py ln[1:10] hl[1,7] *} + +Pydantic has support for `UUID` types. + +For the database, **SQLModel** internally uses SQLAlchemy's `Uuid` type. + +### Create a Record with a UUID + +When creating a `Hero` record, the `id` field will be **automatically populated** with a new UUID because we set `default_factory=uuid.uuid4`. + +As `uuid.uuid4` will be called when creating the model instance, even before sending it to the database, we can **access and use the ID right away**. + +And that **same ID (a UUID)** will be saved in the database. + +{* ./docs_src/advanced/uuid/tutorial001_py310.py ln[23:34] hl[25,27,29,34] *} + +### Select a Hero + +We can do the same operations we could do with other fields. + +For example we can **select a hero by ID**: + +{* ./docs_src/advanced/uuid/tutorial001_py310.py ln[37:54] hl[49] *} + +/// tip + +Even if a database like SQLite stores the UUID as a string, we can select and run comparisons using a Python UUID object and it will work. + +SQLModel (actually SQLAlchemy) will take care of making it work. ✨ + +/// + +#### Select with `session.get()` + +We could also select by ID with `session.get()`: + +{* ./docs_src/advanced/uuid/tutorial002_py310.py ln[37:53] hl[49] *} + +The same way as with other fields, we could update, delete, etc. 🚀 + +### Run the program + +If you run the program, you will see the **UUID** generated in the Python code, and then the record **saved in the database with the same UUID**. + +
+ +```console +$ python app.py + +// Some boilerplate and previous output omitted 😉 + +// In SQLite, the UUID will be stored as a string +// other DBs like Postgres have a specific UUID type +CREATE TABLE hero ( + id CHAR(32) NOT NULL, + name VARCHAR NOT NULL, + secret_name VARCHAR NOT NULL, + age INTEGER, + PRIMARY KEY (id) +) + +// Before saving in the DB we already have the UUID +The hero before saving in the DB +name='Deadpond' secret_name='Dive Wilson' id=UUID('0e44c1a6-88d3-4a35-8b8a-307faa2def28') age=None +The hero ID was already set +0e44c1a6-88d3-4a35-8b8a-307faa2def28 + +// The SQL statement to insert the record uses our UUID +INSERT INTO hero (id, name, secret_name, age) VALUES (?, ?, ?, ?) +('0e44c1a688d34a358b8a307faa2def28', 'Deadpond', 'Dive Wilson', None) + +// And indeed, the record was saved with the UUID we created 😎 +After saving in the DB +age=None id=UUID('0e44c1a6-88d3-4a35-8b8a-307faa2def28') name='Deadpond' secret_name='Dive Wilson' + +// Now we create a new hero (to select it in a bit) +Created hero: +age=None id=UUID('9d90d186-85db-4eaa-891a-def7b4ae2dab') name='Spider-Boy' secret_name='Pedro Parqueador' +Created hero ID: +9d90d186-85db-4eaa-891a-def7b4ae2dab + +// And now we select it +Selected hero: +age=None id=UUID('9d90d186-85db-4eaa-891a-def7b4ae2dab') name='Spider-Boy' secret_name='Pedro Parqueador' +Selected hero ID: +9d90d186-85db-4eaa-891a-def7b4ae2dab +``` + +
+ +## Learn More + +You can learn more about **UUIDs** in: + +* The official Python docs for UUID. +* The Wikipedia for UUID. diff --git a/docs/contributing.md b/docs/contributing.md index 2cfa5331df..8228bc4132 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -4,53 +4,43 @@ First, you might want to see the basic ways to [help SQLModel and get help](help ## Developing -If you already cloned the repository and you know that you need to deep dive in the code, here are some guidelines to set up your environment. +If you already cloned the sqlmodel repository and you want to deep dive in the code, here are some guidelines to set up your environment. -### Python +### Virtual Environment -SQLModel supports Python 3.6 and above, but for development you should have at least **Python 3.7**. +Follow the instructions to create and activate a [virtual environment](virtual-environments.md){.internal-link target=_blank} for the internal code of `sqlmodel`. -### Poetry +### Install Requirements Using `pip` -**SQLModel** uses Poetry to build, package, and publish the project. - -You can learn how to install it in the Poetry docs. - -After having Poetry available, you can install the development dependencies: +After activating the environment, install the required packages:
```console -$ poetry install +$ pip install -r requirements.txt ---> 100% ```
-It will also create a virtual environment automatically and will install all the dependencies and your local SQLModel in it. - -### Poetry Shell - -To use your current environment, and to have access to all the tools in it (for example `pytest` for the tests) enter into a Poetry Shell: +It will install all the dependencies and your local SQLModel in your local environment. -
+### Using your Local SQLModel -```console -$ poetry shell -``` +If you create a Python file that imports and uses SQLModel, and run it with the Python from your local environment, it will use your cloned local SQLModel source code. -
+And if you update that local SQLModel source code when you run that Python file again, it will use the fresh version of SQLModel you just edited. -That will set up the environment variables needed dand will start a new shell with them. +That way, you don't have to "install" your local version to be able to test every change. -#### Using your local SQLModel +/// note | "Technical Details" -If you create a Python file that imports and uses SQLModel, and run it with the Python from your local Poetry environment, it will use your local SQLModel source code. +This only happens when you install using this included `requirements.txt` instead of running `pip install sqlmodel` directly. -And if you update that local SQLModel source code, when you run that Python file again, it will use the fresh version of SQLModel you just edited. +That is because inside the `requirements.txt` file, the local version of SQLModel is marked to be installed in "editable" mode, with the `-e` option. -Poetry takes care of making that work. But of course, it will only work in the current Poetry environment, if you install standard SQLModel in another environment (not from the source in the GitHub repo), that will use the standard SQLModel, not your custom version. +/// ### Format @@ -66,41 +56,36 @@ $ bash scripts/format.sh It will also auto-sort all your imports. -## Docs - -The documentation uses MkDocs with Material for MkDocs. - -All the documentation is in Markdown format in the directory `./docs`. +## Tests -Many of the tutorials have blocks of code. +There is a script that you can run locally to test all the code and generate coverage reports in HTML: -In most of the cases, these blocks of code are actual complete applications that can be run as is. +
-In fact, those blocks of code are not written inside the Markdown, they are Python files in the `./docs_src/` directory. +```console +$ bash scripts/test.sh +``` -And those Python files are included/injected in the documentation when generating the site. +
-### Docs for tests +This command generates a directory `./htmlcov/`, if you open the file `./htmlcov/index.html` in your browser, you can explore interactively the regions of code that are covered by the tests, and notice if there is any region missing. -Most of the tests actually run against the example source files in the documentation. +## Docs -This helps making sure that: +First, make sure you set up your environment as described above, that will install all the requirements. -* The documentation is up to date. -* The documentation examples can be run as is. -* Most of the features are covered by the documentation, ensured by test coverage. +### Docs Live During local development, there is a script that builds the site and checks for any changes, live-reloading:
```console -$ bash scripts/docs-live.sh +$ python ./scripts/docs.py live -[INFO] - Building documentation... -[INFO] - Cleaning site directory -[INFO] - Documentation built in 2.74 seconds -[INFO] - Serving on http://127.0.0.1:8008 +[INFO] Serving on http://127.0.0.1:8008 +[INFO] Start watching changes +[INFO] Start detecting changes ```
@@ -109,20 +94,71 @@ It will serve the documentation on `http://127.0.0.1:8008`. That way, you can edit the documentation/source files and see the changes live. -## Tests +/// tip -There is a script that you can run locally to test all the code and generate coverage reports in HTML: +Alternatively, you can perform the same steps that scripts does manually. + +Go into the docs director at `docs/`: + +```console +$ cd docs/ +``` + +Then run `mkdocs` in that directory: + +```console +$ mkdocs serve --dev-addr 8008 +``` + +/// + +#### Typer CLI (Optional) + +The instructions here show you how to use the script at `./scripts/docs.py` with the `python` program directly. + +But you can also use Typer CLI, and you will get autocompletion in your terminal for the commands after installing completion. + +If you install Typer CLI, you can install completion with:
```console -$ bash scripts/test-cov-html.sh +$ typer --install-completion + +zsh completion installed in /home/user/.bashrc. +Completion will take effect once you restart the terminal. ```
-This command generates a directory `./htmlcov/`, if you open the file `./htmlcov/index.html` in your browser, you can explore interactively the regions of code that are covered by the tests, and notice if there is any region missing. +### Docs Structure + +The documentation uses MkDocs. + +And there are extra tools/scripts in place in `./scripts/docs.py`. + +/// tip + +You don't need to see the code in `./scripts/docs.py`, you just use it in the command line. -## Thanks +/// -Thanks for contributing! ☕ +All the documentation is in Markdown format in the directory `./docs`. + +Many of the tutorials have blocks of code. + +In most of the cases, these blocks of code are actual complete applications that can be run as is. + +In fact, those blocks of code are not written inside the Markdown, they are Python files in the `./docs_src/` directory. + +And those Python files are included/injected in the documentation when generating the site. + +### Docs for Tests + +Most of the tests actually run against the example source files in the documentation. + +This helps to make sure that: + +* The documentation is up-to-date. +* The documentation examples can be run as is. +* Most of the features are covered by the documentation, ensured by test coverage. diff --git a/docs/css/custom.css b/docs/css/custom.css index c479ad08f9..6ac827ff07 100644 --- a/docs/css/custom.css +++ b/docs/css/custom.css @@ -1,3 +1,18 @@ +/* Fira Code, including characters used by Rich output, like the "heavy right-pointing angle bracket ornament", not included in Google Fonts */ +@import url(https://cdn.jsdelivr.net/npm/firacode@6.2.0/distr/fira_code.css); +/* Noto Color Emoji for emoji support with the same font everywhere */ +@import url(https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&display=swap); + +/* Override default code font in Material for MkDocs to Fira Code */ +:root { + --md-code-font: "Fira Code", monospace, "Noto Color Emoji"; +} + +/* Override default regular font in Material for MkDocs to include Noto Color Emoji */ +:root { + --md-text-font: "Roboto", "Noto Color Emoji"; +} + .termynal-comment { color: #4a968f; font-style: italic; @@ -8,6 +23,10 @@ white-space: pre-wrap; } +.termy .linenos { + display: none; +} + a.external-link::after { /* \00A0 is a non-breaking space to make the mark be on the same line as the link @@ -25,3 +44,43 @@ a.internal-link::after { .shadow { box-shadow: 5px 5px 10px #999; } + +.user-list { + display: flex; + flex-wrap: wrap; + margin-bottom: 2rem; +} + +.user-list-center { + justify-content: space-evenly; +} + +.user { + margin: 1em; + min-width: 7em; +} + +.user .avatar-wrapper { + width: 80px; + height: 80px; + margin: 10px auto; + overflow: hidden; + border-radius: 50%; + position: relative; +} + +.user .avatar-wrapper img { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.user .title { + text-align: center; +} + +.user .count { + font-size: 80%; + text-align: center; +} diff --git a/docs/css/termynal.css b/docs/css/termynal.css index 406c00897c..a2564e2860 100644 --- a/docs/css/termynal.css +++ b/docs/css/termynal.css @@ -20,12 +20,14 @@ /* font-size: 18px; */ font-size: 15px; /* font-family: 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; */ - font-family: 'Roboto Mono', 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; + font-family: var(--md-code-font-family), 'Roboto Mono', 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; border-radius: 4px; padding: 75px 45px 35px; position: relative; -webkit-box-sizing: border-box; box-sizing: border-box; + /* Custom line-height */ + line-height: 1.2; } [data-termynal]:before { diff --git a/docs/databases.md b/docs/databases.md index e29c73e506..de25ebb4cb 100644 --- a/docs/databases.md +++ b/docs/databases.md @@ -1,9 +1,12 @@ # Intro to Databases -!!! info - Are you a seasoned developer and already know everything about databases? 🤓 +/// info - Then you can skip to the [Tutorial - User Guide: First Steps](tutorial/index.md){.internal-link target=_blank} right away. +Are you a seasoned developer and already know everything about databases? 🤓 + +Then you can skip to the next sections right away. + +/// If you don't know everything about databases, here's a quick overview. @@ -17,8 +20,11 @@ So, what is a database? A **database** is a system to store and manage data in a structured and very efficient way. -!!! tip - It's very common to abbreviate the word "database" as **"DB"**. +/// tip + +It's very common to abbreviate the word "database" as **"DB"**. + +/// As there's a lot of information about databases, and it can get very technical and academic, I'll give you a quick overview about some of the main concepts here. @@ -28,8 +34,11 @@ I'll even tell you a bit about different types of databases, including the ones When starting to program, it might **not be obvious** why having a database apart from the code for your program is a **good idea**. Let's start with that. -!!! tip - If that's obvious to you, just continue in the next section below. 👇 +/// tip + +If that's obvious to you, just continue in the next section below. 👇 + +/// In your code you already have **variables**, **dictionaries**, **lists**, etc. They all store **data** in some way already. Why would you need to have a separate database? @@ -57,9 +66,9 @@ There are many databases of many types. ### A single file database -A database could be a single file called `heroes.db`, managed with code in a very efficient way. An example would be SQLite, more about that on a bit. +A database could be a single file called `heroes.db`, managed with code in a very efficient way. An example would be SQLite, more about that in a bit. -![database as a single file](/img/databases/single-file.svg) +![database as a single file](img/databases/single-file.drawio.svg) ### A server database @@ -71,11 +80,11 @@ In this case, your code would talk to this server application instead of reading The database could be located in a different server/machine: -![database in an external server](/img/databases/external-server.svg) +![database in an external server](img/databases/external-server.drawio.svg) Or the database could be located in the same server/machine: -![database in the same server](/img/databases/same-server.svg) +![database in the same server](img/databases/same-server.drawio.svg) The most important aspect of these types of databases is that **your code doesn't read or modify** the files containing the data directly. @@ -89,7 +98,7 @@ In some cases, the database could even be a group of server applications running In this case, your code would talk to one or more of these server applications running on different machines. -![distributed database in multiple servers](/img/databases/multiple-servers.svg) +![distributed database in multiple servers](img/databases/multiple-servers.drawio.svg) Most of the databases that work as server applications also support multiple servers in one way or another. @@ -128,7 +137,7 @@ If we worked with a single table to store our heroes, it could be like this: idnamesecret_nameageteamheadquarters -1DeadpondDive WilsonnullZ-FactorSister Margaret’s Bar +1DeadpondDive WilsonnullZ-FactorSister Margaret's Bar 2Spider-BoyPedro ParqueadornullPreventersSharp Tower @@ -157,7 +166,7 @@ We could end up with inconsistent information, having one place saying "Prevente idnamesecret_nameageteamheadquarters -1DeadpondDive WilsonnullZ-ForceSister Margaret’s Bar +1DeadpondDive WilsonnullZ-ForceSister Margaret's Bar 2Spider-BoyPedro ParqueadornullPreventersPreventers Tower ✅ @@ -176,7 +185,7 @@ We could forget the name of the team and end up adding "Mahjong" with an invalid idnamesecret_nameageteamheadquarters -1DeadpondDive WilsonnullZ-ForceSister Margaret’s Bar +1DeadpondDive WilsonnullZ-ForceSister Margaret's Bar 2Spider-BoyPedro ParqueadornullPreventersPreventers Tower @@ -185,7 +194,7 @@ We could forget the name of the team and end up adding "Mahjong" with an invalid 3Rusty-ManTommy Sharp48PreventersSharp Tower -4MahjongNeena Thurgirl31Y-Force 🚨Sister Margaret’s Bar +4MahjongNeena Thurgirl31Y-Force 🚨Sister Margaret's Bar @@ -207,7 +216,7 @@ The table for the teams could look like this: 1PreventersSharp Tower -2Z-ForceSister Margaret’s Bar +2Z-ForceSister Margaret's Bar @@ -248,7 +257,7 @@ For example, the table for the teams has the ID `1` for the team `Preventers` an As these **primary key** IDs can uniquely identify each row on the table for teams, we can now go to the table for heroes and refer to those IDs in the table for teams. -table relationships +![table relationships](img/databases/relationships.drawio.svg) So, in the table for heroes, we use the `team_id` column to define a relationship to the *foreign* table for teams. Each value in the `team_id` column on the table with heroes will be the same value as the `id` column of one row in the table with teams. @@ -274,7 +283,7 @@ The language is called **SQL**, the name comes from for **Structured Query Langu Nevertheless, the language is not only used to *query* for data. It is also used to create records/rows, to update them, to delete them. And to manipulate the database, create tables, etc. -This language is supported by all these databases that handle multiple tables, that's why they are called **SQL Databases**. Although, each database has small variations in the SQL language they support. +This language is supported by all these databases that handle multiple tables, that's why they are called **SQL Databases**. Although, each database has small variations in the SQL language they support (*dialect*). Let's imagine that the table holding the heroes is called the `hero` table. An example of a SQL query to get all the data from it could look like: @@ -308,8 +317,11 @@ Next, it receives the data and puts it in Python objects that you can continue t I'll tell you more about SQL, SQLModel, how to use them, and how they are related in the next sections. -!!! info "Technical Details" - SQLModel is built on top of SQLAlchemy. It is, in fact, just SQLAlchemy and Pydantic mixed together with some sugar on top. +/// info | Technical Details + +SQLModel is built on top of SQLAlchemy. It is, in fact, just SQLAlchemy and Pydantic mixed together with some sugar on top. + +/// ## NoSQL Databases diff --git a/docs/db-to-code.md b/docs/db-to-code.md index d4b182d26e..fbba54c162 100644 --- a/docs/db-to-code.md +++ b/docs/db-to-code.md @@ -62,7 +62,7 @@ The user is probably, in some way, telling your application: 2 ``` -And the would be this table (with a single row): +And the result would be this table (with a single row): @@ -111,7 +111,7 @@ DROP TABLE hero; That is how you tell the database in SQL to delete the entire table `hero`. -Nooooo! We lost all the data in the `hero` table! 💥😱 +Nooooo! We lost all the data in the `hero` table! 💥😱 ### SQL Sanitization @@ -143,7 +143,7 @@ If the user provides this ID: 2 ``` -...the would be this table (with a single row): +...the result would be this table (with a single row):
@@ -172,8 +172,11 @@ The difference in the final SQL statement is subtle, but it changes the meaning SELECT * FROM hero WHERE id = "2; DROP TABLE hero;"; ``` -!!! tip - Notice the double quotes (`"`) making it a string instead of more raw SQL. +/// tip + +Notice the double quotes (`"`) making it a string instead of more raw SQL. + +/// The database will not find any record with that ID: @@ -187,8 +190,11 @@ Then your code will continue to execute and calmly tell the user that it couldn' But we never deleted the `hero` table. 🎉 -!!! info - Of course, there are also other ways to do SQL data sanitization without using a tool like **SQLModel**, but it's still a nice feature you get by default. +/// info + +Of course, there are also other ways to do SQL data sanitization without using a tool like **SQLModel**, but it's still a nice feature you get by default. + +/// ### Editor Support @@ -230,8 +236,7 @@ database.execute( ).all() ``` - - +![](img/db-to-code/autocompletion01.png){class="shadow"} ## ORMs and SQL @@ -247,10 +252,10 @@ For example this class is part of that **Object** Oriented Programming: ```Python class Hero(SQLModel): - id: Optional[int] = Field(default=None, primary_key=True) + id: int | None = Field(default=None, primary_key=True) name: str secret_name: str - age: Optional[int] = None + age: int | None = None ``` * **Relational**: refers to the **SQL Databases**. Remember that they are also called **Relational Databases**, because each of those tables is also called a "**relation**"? That's where the "**Relational**" comes from. @@ -274,7 +279,7 @@ For example this **Relation** or table: * **Mapper**: this comes from Math, when there's something that can convert from some set of things to another, that's called a "**mapping function**". That's where the **Mapper** comes from. -![Squares to Triangles Mapper](/img/db-to-code/mapper.svg) +![Squares to Triangles Mapper](img/db-to-code/mapper.drawio.svg) We could also write a **mapping function** in Python that converts from the *set of lowercase letters* to the *set of uppercase letters*, like this: @@ -291,8 +296,11 @@ There are many ORMs available apart from **SQLModel**, you can read more about s ## SQL Table Names -!!! info "Technical Background" - This is a bit of boring background for SQL purists. Feel free to skip this section. 😉 +/// info | Technical Background + +This is a bit of boring background for SQL purists. Feel free to skip this section. 😉 + +/// When working with pure SQL, it's common to name the tables in plural. So, the table would be named `heroes` instead of `hero`, because it could contain multiple rows, each with one hero. @@ -304,5 +312,8 @@ You will see **your own code** a lot more than the internal table names, so it's So, to keep things consistent, I'll keep using the same table names that **SQLModel** would have generated. -!!! tip - You can also override the table name. You can read about it in the Advanced User Guide. \ No newline at end of file +/// tip + +You can also override the table name. You can read about it in the Advanced User Guide. + +/// diff --git a/docs/environment-variables.md b/docs/environment-variables.md new file mode 100644 index 0000000000..bd8b2a06d0 --- /dev/null +++ b/docs/environment-variables.md @@ -0,0 +1,300 @@ +# Environment Variables + +Before we jump into code, let's cover a bit some of the **basics** that we'll need to understand how to work with Python (and programming) in general. Let's check a bit about **environment variables**. + +/// tip + +If you already know what "environment variables" are and how to use them, feel free to skip this. + +/// + +An environment variable (also known as "**env var**") is a variable that lives **outside** of the Python code, in the **operating system**, and could be read by your Python code (or by other programs as well). + +Environment variables could be useful for handling application **settings**, as part of the **installation** of Python, etc. + +## Create and Use Env Vars + +You can **create** and use environment variables in the **shell (terminal)**, without needing Python: + +//// tab | Linux, macOS, Windows Bash + +
+ +```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 +``` + +
+ +//// + +//// tab | Windows PowerShell + +
+ +```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 +``` + +
+ +//// + +## Read env vars in Python + +You could also create environment variables **outside** of Python, in the terminal (or with any other method), and then **read them in Python**. + +For example you could have a file `main.py` with: + +```Python hl_lines="3" +import os + +name = os.getenv("MY_NAME", "World") +print(f"Hello {name} from Python") +``` + +/// tip + +The second argument to `os.getenv()` is the default value to return. + +If not provided, it's `None` by default, here we provide `"World"` as the default value to use. + +/// + +Then you could call that Python program: + +//// tab | Linux, macOS, Windows Bash + +
+ +```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 +``` + +
+ +//// + +//// tab | Windows PowerShell + +
+ +```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 +``` + +
+ +//// + +As environment variables can be set outside of the code, but can be read by the code, and don't have to be stored (committed to `git`) with the rest of the files, it's common to use them for configurations or **settings**. + +You can also create an environment variable only for a **specific program invocation**, that is only available to that program, and only for its duration. + +To do that, create it right before the program itself, on the same line: + +
+ +```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 +``` + +
+ +/// tip + +You can read more about it at The Twelve-Factor App: Config. + +/// + +## Types and Validation + +These environment variables can only handle **text strings**, as they are external to Python and have to be compatible with other programs and the rest of the system (and even with different operating systems, as Linux, Windows, macOS). + +That means that **any value** read in Python from an environment variable **will be a `str`**, and any conversion to a different type or any validation has to be done in code. + +## `PATH` Environment Variable + +There is a **special** environment variable called **`PATH`** that is used by the operating systems (Linux, macOS, Windows) to find programs to run. + +The value of the variable `PATH` is a long string that is made of directories separated by a colon `:` on Linux and macOS, and by a semicolon `;` on Windows. + +For example, the `PATH` environment variable could look like this: + +//// tab | Linux, macOS + +```plaintext +/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin +``` + +This means that the system should look for programs in the directories: + +* `/usr/local/bin` +* `/usr/bin` +* `/bin` +* `/usr/sbin` +* `/sbin` + +//// + +//// tab | Windows + +```plaintext +C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32 +``` + +This means that the system should look for programs in the directories: + +* `C:\Program Files\Python312\Scripts` +* `C:\Program Files\Python312` +* `C:\Windows\System32` + +//// + +When you type a **command** in the terminal, the operating system **looks for** the program in **each of those directories** listed in the `PATH` environment variable. + +For example, when you type `python` in the terminal, the operating system looks for a program called `python` in the **first directory** in that list. + +If it finds it, then it will **use it**. Otherwise it keeps looking in the **other directories**. + +### Installing Python and Updating the `PATH` + +When you install Python, you might be asked if you want to update the `PATH` environment variable. + +//// tab | Linux, macOS + +Let's say you install Python and it ends up in a directory `/opt/custompython/bin`. + +If you say yes to update the `PATH` environment variable, then the installer will add `/opt/custompython/bin` to the `PATH` environment variable. + +It could look like this: + +```plaintext +/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/custompython/bin +``` + +This way, when you type `python` in the terminal, the system will find the Python program in `/opt/custompython/bin` (the last directory) and use that one. + +//// + +//// tab | Windows + +Let's say you install Python and it ends up in a directory `C:\opt\custompython\bin`. + +If you say yes to update the `PATH` environment variable, then the installer will add `C:\opt\custompython\bin` to the `PATH` environment variable. + +```plaintext +C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32;C:\opt\custompython\bin +``` + +This way, when you type `python` in the terminal, the system will find the Python program in `C:\opt\custompython\bin` (the last directory) and use that one. + +//// + +This way, when you type `python` in the terminal, the system will find the Python program in `/opt/custompython/bin` (the last directory) and use that one. + +So, if you type: + +
+ +```console +$ python +``` + +
+ +//// tab | Linux, macOS + +The system will **find** the `python` program in `/opt/custompython/bin` and run it. + +It would be roughly equivalent to typing: + +
+ +```console +$ /opt/custompython/bin/python +``` + +
+ +//// + +//// tab | Windows + +The system will **find** the `python` program in `C:\opt\custompython\bin\python` and run it. + +It would be roughly equivalent to typing: + +
+ +```console +$ C:\opt\custompython\bin\python +``` + +
+ +//// + +This information will be useful when learning about [Virtual Environments](virtual-environments.md){.internal-link target=_blank}. + +## Conclusion + +With this you should have a basic understanding of what **environment variables** are and how to use them in Python. + +You can also read more about them in the Wikipedia for Environment Variable. + +In many cases it's not very obvious how environment variables would be useful and applicable right away. But they keep showing up in many different scenarios when you are developing, so it's good to know about them. + +For example, you will need this information in the next section, about [Virtual Environments](virtual-environments.md). diff --git a/docs/features.md b/docs/features.md index 09de0c17f9..2f2e873105 100644 --- a/docs/features.md +++ b/docs/features.md @@ -12,7 +12,7 @@ Nevertheless, SQLModel is completely **independent** of FastAPI and can be used ## Just Modern Python -It's all based on standard modern **Python** type annotations. No new syntax to learn. Just standard modern Python. +It's all based on standard modern **Python** type annotations. No new syntax to learn. Just standard modern Python. If you need a 2 minute refresher of how to use Python types (even if you don't use SQLModel or FastAPI), check the FastAPI tutorial section: Python types intro. @@ -36,17 +36,10 @@ You will get completion for everything while writing the **minimum** amount of c You won't need to keep guessing the types of different attributes in your models, if they could be `None`, etc. Your editor will be able to help you with everything because **SQLModel** is based on **standard Python type annotations**. -**SQLModel** even adopts currently in development standards for Python type annotations to ensure the **best developer experience**, so you will get inline errors and autocompletion even while creating new model instances. +**SQLModel** adopts PEP 681 for Python type annotations to ensure the **best developer experience**, so you will get inline errors and autocompletion even while creating new model instances. -!!! info - Don't worry, adopting this in-development standard only affects/improves editor support. - - It doesn't affect performance or correctness. And if the in-progress standard was deprecated your code won't be affected. - - Meanwhile, you will get inline errors (like type checks) and autocompletion on places you wouldn't get with any other library. 🎉 - ## Short **SQLModel** has **sensible defaults** for everything, with **optional configurations** everywhere. @@ -83,7 +76,7 @@ Underneath, ✨ a **SQLModel** model is also a **SQLAlchemy** model. ✨ There was **a lot** of research and effort dedicated to make it that way. In particular, there was a lot of effort and experimentation in making a single model be **both a SQLAlchemy model and a Pydantic** model at the same time. -That means that you get all the power, robustness, and certainty of SQLAlchemy, the most widely used database library in Python. +That means that you get all the power, robustness, and certainty of SQLAlchemy, the most widely used database library in Python. **SQLModel** provides its own utilities to improve the developer experience, but underneath, it uses all of SQLAlchemy. diff --git a/docs/help.md b/docs/help.md index bf2360bd60..6e5fe581f7 100644 --- a/docs/help.md +++ b/docs/help.md @@ -12,7 +12,7 @@ And there are several ways to get help too. ## Subscribe to the FastAPI and Friends newsletter -You can subscribe to the (infrequent) [**FastAPI and friends** newsletter](/newsletter/){.internal-link target=_blank} to stay updated about: +You can subscribe to the (infrequent) **FastAPI and friends** newsletter to stay updated about: * News about FastAPI and friends, including SQLModel 🚀 * Guides 📝 @@ -22,13 +22,13 @@ You can subscribe to the (infrequent) [**FastAPI and friends** newsletter](/news ## Star **SQLModel** in GitHub -You can "star" SQLModel in GitHub (clicking the star button at the top right): https://github.com/tiangolo/sqlmodel. ⭐️ +You can "star" SQLModel in GitHub (clicking the star button at the top right): https://github.com/fastapi/sqlmodel. ⭐️ By adding a star, other users will be able to find it more easily and see that it has been already useful for others. ## Watch the GitHub repository for releases -You can "watch" SQLModel in GitHub (clicking the "watch" button at the top right): https://github.com/tiangolo/sqlmodel. 👀 +You can "watch" SQLModel in GitHub (clicking the "watch" button at the top right): https://github.com/fastapi/sqlmodel. 👀 There you can select "Releases only". @@ -54,30 +54,132 @@ You can: ## Tweet about **SQLModel** -Tweet about **SQLModel** and let me and others know why you like it. 🎉 +Tweet about **SQLModel** and let me and others know why you like it. 🎉 I love to hear about how **SQLModel** is being used, what you have liked in it, in which project/company are you using it, etc. -## Help others with issues in GitHub +## Help others with questions in GitHub -You can see existing issues and try and help others, most of the times they are questions that you might already know the answer for. 🤓 +You can try and help others with their questions in: + +* GitHub Discussions +* GitHub Issues + +In many cases you might already know the answer for those questions. 🤓 + +Just remember, the most important point is: try to be kind. People come with their frustrations and in many cases don't ask in the best way, but try as best as you can to be kind. 🤗 + +The idea is for the **SQLModel** community to be kind and welcoming. At the same time, don't accept bullying or disrespectful behavior towards others. We have to take care of each other. + +--- + +Here's how to help others with questions (in discussions or issues): + +### Understand the question + +* Check if you can understand what is the **purpose** and use case of the person asking. + +* Then check if the question (the vast majority are questions) is **clear**. + +* In many cases the question asked is about an imaginary solution from the user, but there might be a **better** one. If you can understand the problem and use case better, you might be able to suggest a better **alternative solution**. + +* If you can't understand the question, ask for more **details**. + +### Reproduce the problem + +For most of the cases and most of the questions there's something related to the person's **original code**. + +In many cases they will only copy a fragment of the code, but that's not enough to **reproduce the problem**. + +* You can ask them to provide a minimal, reproducible, example, that you can **copy-paste** and run locally to see the same error or behavior they are seeing, or to understand their use case better. + +* If you are feeling too generous, you can try to **create an example** like that yourself, just based on the description of the problem. Just have in mind that this might take a lot of time and it might be better to ask them to clarify the problem first. + +### Suggest solutions + +* After being able to understand the question, you can give them a possible **answer**. + +* In many cases, it's better to understand their **underlying problem or use case**, because there might be a better way to solve it than what they are trying to do. + +### Ask to close + +If they reply, there's a high chance you would have solved their problem, congrats, **you're a hero**! 🦸 + +* Now, if that solved their problem, you can ask them to: + + * In GitHub Discussions: mark the comment as the **answer**. + * In GitHub Issues: **close** the issue**. ## Watch the GitHub repository -You can "watch" SQLModel in GitHub (clicking the "watch" button at the top right): https://github.com/tiangolo/sqlmodel. 👀 +You can "watch" SQLModel in GitHub (clicking the "watch" button at the top right): https://github.com/fastapi/sqlmodel. 👀 -If you select "Watching" instead of "Releases only" you will receive notifications when someone creates a new issue. +If you select "Watching" instead of "Releases only" you will receive notifications when someone creates a new issue or question. You can also specify that you only want to be notified about new issues, or discussions, or PRs, etc. -Then you can try and help them solve those issues. +Then you can try and help them solve those questions. -## Create issues +## Ask Questions -You can create a new issue in the GitHub repository, for example to: +You can create a new question in the GitHub repository, for example to: * Ask a **question** or ask about a **problem**. * Suggest a new **feature**. -**Note**: if you create an issue, then I'm going to ask you to also help others. 😉 +**Note**: if you do it, then I'm going to ask you to also help others. 😉 + +## Review Pull Requests + +You can help me review pull requests from others. + +Again, please try your best to be kind. 🤗 + +--- + +Here's what to have in mind and how to review a pull request: + +### Understand the problem + +* First, make sure you **understand the problem** that the pull request is trying to solve. It might have a longer discussion in a GitHub Discussion or issue. + +* There's also a good chance that the pull request is not actually needed because the problem can be solved in a **different way**. Then you can suggest or ask about that. + +### Don't worry about style + +* Don't worry too much about things like commit message styles, I will squash and merge customizing the commit manually. + +* Also don't worry about style rules, there are already automatized tools checking that. + +And if there's any other style or consistency need, I'll ask directly for that, or I'll add commits on top with the needed changes. + +### Check the code + +* Check and read the code, see if it makes sense, **run it locally** and see if it actually solves the problem. + +* Then **comment** saying that you did that, that's how I will know you really checked it. + +/// info + +Unfortunately, I can't simply trust PRs that just have several approvals. + +Several times it has happened that there are PRs with 3, 5 or more approvals, probably because the description is appealing, but when I check the PRs, they are actually broken, have a bug, or don't solve the problem they claim to solve. 😅 + +So, it's really important that you actually read and run the code, and let me know in the comments that you did. 🤓 + +/// + +* If the PR can be simplified in a way, you can ask for that, but there's no need to be too picky, there might be a lot of subjective points of view (and I will have my own as well 🙈), so it's better if you can focus on the fundamental things. + +### Tests + +* Help me check that the PR has **tests**. + +* Check that the tests **fail** before the PR. 🚨 + +* Then check that the tests **pass** after the PR. ✅ + +* Many PRs don't have tests, you can **remind** them to add tests, or you can even **suggest** some tests yourself. That's one of the things that consume most time and you can help a lot with that. + +* Then also comment what you tried, that way I'll know that you checked it. 🤓 ## Create a Pull Request @@ -86,7 +188,47 @@ You can [contribute](contributing.md){.internal-link target=_blank} to the sourc * To fix a typo you found on the documentation. * To propose new documentation sections. * To fix an existing issue/bug. + * Make sure to add tests. * To add a new feature. + * Make sure to add tests. + * Make sure to add documentation if it's relevant. + +## Help Maintain SQLModel + +Help me maintain **SQLModel**! 🤓 + +There's a lot of work to do, and for most of it, **YOU** can do it. + +The main tasks that you can do right now are: + +* [Help others with questions in GitHub](#help-others-with-questions-in-github){.internal-link target=_blank} (see the section above). +* [Review Pull Requests](#review-pull-requests){.internal-link target=_blank} (see the section above). + +Those two tasks are what **consume time the most**. That's the main work of maintaining SQLModel. + +If you can help me with that, **you are helping me maintain SQLModel** and making sure it keeps **advancing faster and better**. 🚀 + +## Join the chat + +Join the 👥 FastAPI and Friends Discord chat server 👥 and hang out with others in the community. There's a `#sqlmodel` channel. + +/// tip + +For questions, ask them in GitHub Discussions, there's a much better chance you will receive help there. + +Use the chat only for other general conversations. + +/// + +### Don't use the chat for questions + +Have in mind that as chats allow more "free conversation", it's easy to ask questions that are too general and more difficult to answer, so, you might not receive answers. + +In GitHub, the template will guide you to write the right question so that you can more easily get a good answer, or even solve the problem yourself even before asking. And in GitHub I can make sure I always answer everything, even if it takes some time. I can't personally do that with the chat. 😅 + +Conversations in the chat are also not as easily searchable as in GitHub, so questions and answers might get lost in the conversation. + +On the other side, there are thousands of users in the chat, so there's a high chance you'll find someone to talk to there, almost all the time. 😄 ## Sponsor the author diff --git a/docs/img/databases/external-server.drawio b/docs/img/databases/external-server.drawio deleted file mode 100644 index 4af8e30d6c..0000000000 --- a/docs/img/databases/external-server.drawio +++ /dev/null @@ -1,93 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/img/databases/external-server.drawio.svg b/docs/img/databases/external-server.drawio.svg new file mode 100644 index 0000000000..f86b5dc1e6 --- /dev/null +++ b/docs/img/databases/external-server.drawio.svg @@ -0,0 +1,778 @@ + + + + + + + + + + + + + + + +
+
+
+ + Machine / Computer + +
+
+
+
+ + Machine / Computer + +
+
+
+ + + + + + + + + + + + + + + + + + + +
+
+
+ + Database application + +
+
+
+
+ + Database application + +
+
+
+ + + + + + + + + + + + + + +
+
+
+ + File + +
+
+
+
+ + File + +
+
+
+ + + + + + + +
+
+
+ + Data + +
+
+
+
+ + Data + +
+
+
+ + + + + + + + + + + + + + +
+
+
+ + File + +
+
+
+
+ + File + +
+
+
+ + + + + + + +
+
+
+ + Data + +
+
+
+
+ + Data + +
+
+
+ + + + + + + + + + + + + + +
+
+
+ + File + +
+
+
+
+ + File + +
+
+
+ + + + + + + +
+
+
+ + Data + +
+
+
+
+ + Data + +
+
+
+ + + + + + + + + + + +
+
+
+ + Machine / Computer + +
+
+
+
+ + Machine / Computer + +
+
+
+ + + + + + + +
+
+
+ + Your code + +
+
+
+
+ + Your code + +
+
+
+ + + + +
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/img/databases/external-server.svg b/docs/img/databases/external-server.svg deleted file mode 100644 index cf0f541bfa..0000000000 --- a/docs/img/databases/external-server.svg +++ /dev/null @@ -1 +0,0 @@ -
Machine / Computer
Machine / Computer
Database application
Database application
File
File
Data
Data
File
File
Data
Data
File
File
Data
Data
Machine / Computer
Machine / Computer
Your code
Your code
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/img/databases/multiple-servers.drawio b/docs/img/databases/multiple-servers.drawio deleted file mode 100644 index efceed9c08..0000000000 --- a/docs/img/databases/multiple-servers.drawio +++ /dev/null @@ -1,205 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/img/databases/multiple-servers.drawio.svg b/docs/img/databases/multiple-servers.drawio.svg new file mode 100644 index 0000000000..b7370592a0 --- /dev/null +++ b/docs/img/databases/multiple-servers.drawio.svg @@ -0,0 +1,903 @@ + + + + + + + + + + + + + + + +
+
+
+ + Machine / Computer + +
+
+
+
+ + Machine / Computer + +
+
+
+ + + + + + + + + + + + + + + +
+
+
+ + Database application + +
+
+
+
+ + Database application + +
+
+
+ + + + + + + + + + + + + + +
+
+
+ + File + +
+
+
+
+ + File + +
+
+
+ + + + + + + +
+
+
+ + Data + +
+
+
+
+ + Data + +
+
+
+ + + + + + + + + + + + + + +
+
+
+ + File + +
+
+
+
+ + File + +
+
+
+ + + + + + + +
+
+
+ + Data + +
+
+
+
+ + Data + +
+
+
+ + + + + + + + + + + +
+
+
+ + Machine / Computer + +
+
+
+
+ + Machine / Computer + +
+
+
+ + + + + + + +
+
+
+ + Your code + +
+
+
+
+ + Your code + +
+
+
+ + + + + + + + + + + + + + +
+
+
+ + Machine / Computer + +
+
+
+
+ + Machine / Computer + +
+
+
+ + + + + + + + + + + + + + + +
+
+
+ + Database application + +
+
+
+
+ + Database application + +
+
+
+ + + + + + + + + + + + + + +
+
+
+ + File + +
+
+
+
+ + File + +
+
+
+ + + + + + + +
+
+
+ + Data + +
+
+
+
+ + Data + +
+
+
+ + + + + + + + + + + + + + +
+
+
+ + File + +
+
+
+
+ + File + +
+
+
+ + + + + + + +
+
+
+ + Data + +
+
+
+
+ + Data + +
+
+
+ + + + + + + + + + +
+
+
+ + Machine / Computer + +
+
+
+
+ + Machine / Computer + +
+
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ + Database application + +
+
+
+
+ + Database application + +
+
+
+ + + + + + + + + + + + + + +
+
+
+ + File + +
+
+
+
+ + File + +
+
+
+ + + + + + + +
+
+
+ + Data + +
+
+
+
+ + Data + +
+
+
+ + + + + + + + + + + + + + +
+
+
+ + File + +
+
+
+
+ + File + +
+
+
+ + + + + + + +
+
+
+ + Data + +
+
+
+
+ + Data + +
+
+
+ + + + + + + + + + + + + + +
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/img/databases/multiple-servers.svg b/docs/img/databases/multiple-servers.svg deleted file mode 100644 index 039260a608..0000000000 --- a/docs/img/databases/multiple-servers.svg +++ /dev/null @@ -1 +0,0 @@ -
Machine / Computer
Machine / Computer
Database application
Database application
File
File
Data
Data
File
File
Data
Data
Machine / Computer
Machine / Computer
Your code
Your code
Machine / Computer
Machine / Computer
Database application
Database application
File
File
Data
Data
File
File
Data
Data
Machine / Computer
Machine / Computer
Database application
Database application
File
File
Data
Data
File
File
Data
Data
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/img/databases/relationships.drawio b/docs/img/databases/relationships.drawio deleted file mode 100644 index 9ae668cf99..0000000000 --- a/docs/img/databases/relationships.drawio +++ /dev/null @@ -1,151 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/img/databases/relationships.drawio.svg b/docs/img/databases/relationships.drawio.svg new file mode 100644 index 0000000000..e1cbb43e92 --- /dev/null +++ b/docs/img/databases/relationships.drawio.svg @@ -0,0 +1,1189 @@ + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + hero + +
+
+
+
+ + hero + +
+
+
+ + + + + + + + + + + +
+
+
+ + + id + + +
+
+
+
+ + id + +
+
+
+ + + + + + + + +
+
+
+ + + name + + +
+
+
+
+ + name + +
+
+
+ + + + + + + + +
+
+
+ + + secret_name + + +
+
+
+
+ + secret_name + +
+
+
+ + + + + + + + +
+
+
+ + + age + + +
+
+
+
+ + age + +
+
+
+ + + + + + + + +
+
+
+ + + team_id + + +
+
+
+
+ + team_id + +
+
+
+ + + + + + + + + + + +
+
+
+ + + 1 + + +
+
+
+
+ + 1 + +
+
+
+ + + + + + + + +
+
+
+ + + Deadpond + + +
+
+
+
+ + Deadpond + +
+
+
+ + + + + + + + +
+
+
+ + + Dive Wilson + + +
+
+
+
+ + Dive Wilson + +
+
+
+ + + + + + + + +
+
+
+ + + null + + +
+
+
+
+ + null + +
+
+
+ + + + + + + + +
+
+
+ + + 2 + + +
+
+
+
+ + 2 + +
+
+
+ + + + + + + + + + + +
+
+
+ + + 2 + + +
+
+
+
+ + 2 + +
+
+
+ + + + + + + + +
+
+
+ + + Spider-Boy + + +
+
+
+
+ + Spider-Boy + +
+
+
+ + + + + + + + +
+
+
+ + + Pedro Parqueador + + +
+
+
+
+ + Pedro Parqueador + +
+
+
+ + + + + + + + +
+
+
+ + + null + + +
+
+
+
+ + null + +
+
+
+ + + + + + + + +
+
+
+ + + 1 + + +
+
+
+
+ + 1 + +
+
+
+ + + + + + + + + + + +
+
+
+ + + 3 + + +
+
+
+
+ + 3 + +
+
+
+ + + + + + + + +
+
+
+ + + Rusty-Man + + +
+
+
+
+ + Rusty-Man + +
+
+
+ + + + + + + + +
+
+
+ + + Tommy Sharp + + +
+
+
+
+ + Tommy Sharp + +
+
+
+ + + + + + + + +
+
+
+ + + 48 + + +
+
+
+
+ + 48 + +
+
+
+ + + + + + + + +
+
+
+ + + 1 + + +
+
+
+
+ + 1 + +
+
+
+ + + + + + + + + + + + + +
+
+
+ + team + +
+
+
+
+ + team + +
+
+
+ + + + + + + + + + + +
+
+
+ + + id + + +
+
+
+
+ + id + +
+
+
+ + + + + + + + +
+
+
+ + + name + + +
+
+
+
+ + name + +
+
+
+ + + + + + + + +
+
+
+ + + headquarters + + +
+
+
+
+ + headquarters + +
+
+
+ + + + + + + + + + + +
+
+
+ + + 1 + + +
+
+
+
+ + 1 + +
+
+
+ + + + + + + + +
+
+
+ + + Preventers + + +
+
+
+
+ + Preventers + +
+
+
+ + + + + + + + +
+
+
+ + + Sharp Tower + + +
+
+
+
+ + Sharp Tower + +
+
+
+ + + + + + + + + + + +
+
+
+ + + 2 + + +
+
+
+
+ + 2 + +
+
+
+ + + + + + + + +
+
+
+ + + Z-Force + + +
+
+
+
+ + Z-Force + +
+
+
+ + + + + + + + +
+
+
+

+ + Sister Margaret's Bar + +

+
+
+
+
+ + Sister Margaret's Bar + +
+
+
+ + + + + + + + + + + + +
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/img/databases/relationships.svg b/docs/img/databases/relationships.svg deleted file mode 100644 index fbecdac05a..0000000000 --- a/docs/img/databases/relationships.svg +++ /dev/null @@ -1,57 +0,0 @@ -
hero
hero
id
id
name
name
secret_name
secret_name
age
age
team_id
team_id
1
1
Deadpond
Deadpond
Dive Wilson
Dive Wilson
null
null
2
2
2
2
Spider-Boy
Spider-Boy
Pedro Parqueador
Pedro Parqueador
null
null
1
1
3
3
Rusty-Man
Rusty-Man
Tommy Sharp
Tommy Sharp
48
48
1
1
team
team
id
id
name
name
headquarters
headquarters
1
1
Preventers
Preventers
Sharp Tower
Sharp Tower
2
2
Z-Force
Z-Force

Sister Margaret’s Bar

Sister Margaret’s Bar
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/img/databases/same-server.drawio b/docs/img/databases/same-server.drawio deleted file mode 100644 index 596c64956c..0000000000 --- a/docs/img/databases/same-server.drawio +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/img/databases/same-server.drawio.svg b/docs/img/databases/same-server.drawio.svg new file mode 100644 index 0000000000..d9aabff8e9 --- /dev/null +++ b/docs/img/databases/same-server.drawio.svg @@ -0,0 +1,425 @@ + + + + + + + + + + + + + + + +
+
+
+ + Machine / Computer + +
+
+
+
+ + Machine / Computer + +
+
+
+ + + + + + + + + + + + + + + + + + + +
+
+
+ + Database application + +
+
+
+
+ + Database application + +
+
+
+ + + + + + + + + + + + + + +
+
+
+ + File + +
+
+
+
+ + File + +
+
+
+ + + + + + + +
+
+
+ + Data + +
+
+
+
+ + Data + +
+
+
+ + + + + + + + + + + + + + +
+
+
+ + File + +
+
+
+
+ + File + +
+
+
+ + + + + + + +
+
+
+ + Data + +
+
+
+
+ + Data + +
+
+
+ + + + + + + + + + + + + + +
+
+
+ + File + +
+
+
+
+ + File + +
+
+
+ + + + + + + +
+
+
+ + Data + +
+
+
+
+ + Data + +
+
+
+ + + + + + + + + + + +
+
+
+ + Your code + +
+
+
+
+ + Your code + +
+
+
+
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/img/databases/same-server.svg b/docs/img/databases/same-server.svg deleted file mode 100644 index 7f2a77b73c..0000000000 --- a/docs/img/databases/same-server.svg +++ /dev/null @@ -1 +0,0 @@ -
Machine / Computer
Machine / Computer
Database application
Database application
File
File
Data
Data
File
File
Data
Data
File
File
Data
Data
Your code
Your code
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/img/databases/single-file.drawio b/docs/img/databases/single-file.drawio deleted file mode 100644 index 52ce703c4c..0000000000 --- a/docs/img/databases/single-file.drawio +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/img/databases/single-file.drawio.svg b/docs/img/databases/single-file.drawio.svg new file mode 100644 index 0000000000..e92ae0ca3b --- /dev/null +++ b/docs/img/databases/single-file.drawio.svg @@ -0,0 +1,119 @@ + + + + + + + + + + + + + +
+
+
+ + Machine / Computer + +
+
+
+
+ + Machine / Computer + +
+
+
+ + + + + + + + + + + +
+
+
+ + Your code + +
+
+
+
+ + Your code + +
+
+
+ + + + + + + + + + + + + + +
+
+
+ + File: heroes.db + +
+
+
+
+ + File: heroes.db + +
+
+
+ + + + + + + +
+
+
+ + Data + +
+
+
+
+ + Data + +
+
+
+
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/img/databases/single-file.svg b/docs/img/databases/single-file.svg deleted file mode 100644 index f2526d952f..0000000000 --- a/docs/img/databases/single-file.svg +++ /dev/null @@ -1 +0,0 @@ -
Machine / Computer
Machine / Computer
Your code
Your code
File: heroes.db
File: heroes.db
Data
Data
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/img/db-to-code/mapper.drawio b/docs/img/db-to-code/mapper.drawio deleted file mode 100644 index 072b629b54..0000000000 --- a/docs/img/db-to-code/mapper.drawio +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/img/db-to-code/mapper.drawio.svg b/docs/img/db-to-code/mapper.drawio.svg new file mode 100644 index 0000000000..e402e4280a --- /dev/null +++ b/docs/img/db-to-code/mapper.drawio.svg @@ -0,0 +1,291 @@ + + + + + + + + + + + + + + + + + + + + +
+
+
+ + Set of triangles + +
+
+
+
+ + Set of triangles + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + Set of squares + +
+
+
+
+ + Set of squares + +
+
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ + Squares to Triangles + + Mapper + + +
+
+
+
+ + Squares to Triangles Mapp... + +
+
+
+ + + +
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/img/db-to-code/mapper.svg b/docs/img/db-to-code/mapper.svg deleted file mode 100644 index 496c907464..0000000000 --- a/docs/img/db-to-code/mapper.svg +++ /dev/null @@ -1 +0,0 @@ -
Set of triangles
Set of triangles
Set of squares
Set of squares
Squares to Triangles Mapper
Squares to Triangles Mapp...
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/img/index/autocompletion01.png b/docs/img/index/autocompletion01.png index 1a47940b3c..cba2649fe9 100644 Binary files a/docs/img/index/autocompletion01.png and b/docs/img/index/autocompletion01.png differ diff --git a/docs/img/index/autocompletion02.png b/docs/img/index/autocompletion02.png index effa22ee85..5918e1d200 100644 Binary files a/docs/img/index/autocompletion02.png and b/docs/img/index/autocompletion02.png differ diff --git a/docs/img/index/inline-errors01.png b/docs/img/index/inline-errors01.png index f5ef90ed71..b689c55624 100644 Binary files a/docs/img/index/inline-errors01.png and b/docs/img/index/inline-errors01.png differ diff --git a/docs/img/logo-margin/logo-margin-vector.svg b/docs/img/logo-margin/logo-margin-vector.svg index 75e90c838c..335d27a6c6 100644 --- a/docs/img/logo-margin/logo-margin-vector.svg +++ b/docs/img/logo-margin/logo-margin-vector.svg @@ -1,15 +1,15 @@ + width="848.54462mm" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:dc="http://purl.org/dc/elements/1.1/"> image/svg+xml - - - - - - - - - - - - + style="font-weight:bold;font-size:113.462px;line-height:1.25;font-family:Ubuntu;-inkscape-font-specification:'Ubuntu Bold';letter-spacing:0px;word-spacing:0px;fill:#7e56c2;stroke-width:2.83655" + transform="scale(1.0209259,0.97950298)" + aria-label="SQLModel" /> diff --git a/docs/img/logo-margin/logo-margin-white-vector.svg b/docs/img/logo-margin/logo-margin-white-vector.svg new file mode 100644 index 0000000000..d87b002951 --- /dev/null +++ b/docs/img/logo-margin/logo-margin-white-vector.svg @@ -0,0 +1,100 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/img/logo-margin/logo-margin-white.svg b/docs/img/logo-margin/logo-margin-white.svg new file mode 100644 index 0000000000..47ca776249 --- /dev/null +++ b/docs/img/logo-margin/logo-margin-white.svg @@ -0,0 +1,105 @@ + + + + + + + image/svg+xml + + + + + SQLModel + + + + + + + + + + + + + + + + + + + diff --git a/docs/img/logo-margin/logo-margin.svg b/docs/img/logo-margin/logo-margin.svg index 0f23f7c558..b6e4d00710 100644 --- a/docs/img/logo-margin/logo-margin.svg +++ b/docs/img/logo-margin/logo-margin.svg @@ -1,39 +1,15 @@ - + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:dc="http://purl.org/dc/elements/1.1/"> image/svg+xml - - diff --git a/docs/img/sponsors/govcert.png b/docs/img/sponsors/govcert.png new file mode 100644 index 0000000000..0bb4cb934a Binary files /dev/null and b/docs/img/sponsors/govcert.png differ diff --git a/docs/img/tutorial/fastapi/multiple-models/image03.png b/docs/img/tutorial/fastapi/multiple-models/image03.png index f3830b8045..796ca5a370 100644 Binary files a/docs/img/tutorial/fastapi/multiple-models/image03.png and b/docs/img/tutorial/fastapi/multiple-models/image03.png differ diff --git a/docs/img/tutorial/indexes/dictionary001.drawio b/docs/img/tutorial/indexes/dictionary001.drawio deleted file mode 100644 index 659f6b52a4..0000000000 --- a/docs/img/tutorial/indexes/dictionary001.drawio +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/img/tutorial/indexes/dictionary001.drawio.svg b/docs/img/tutorial/indexes/dictionary001.drawio.svg new file mode 100644 index 0000000000..75ebe51f43 --- /dev/null +++ b/docs/img/tutorial/indexes/dictionary001.drawio.svg @@ -0,0 +1,660 @@ + + + + + + + + + + + + + +
+
+
+ + A + +
+
+
+
+ + A + +
+
+
+ + + + + + + +
+
+
+ + B + +
+
+
+
+ + B + +
+
+
+ + + + + + + +
+
+
+ + C + +
+
+
+
+ + C + +
+
+
+ + + + + + + +
+
+
+ + D + +
+
+
+
+ + D + +
+
+
+ + + + + + + +
+
+
+ + E + +
+
+
+
+ + E + +
+
+
+ + + + + + + +
+
+
+ + F + +
+
+
+
+ + F + +
+
+
+ + + + + + + +
+
+
+ + G + +
+
+
+
+ + G + +
+
+
+ + + + + + + +
+
+
+ + H + +
+
+
+
+ + H + +
+
+
+ + + + + + + +
+
+
+ + I + +
+
+
+
+ + I + +
+
+
+ + + + + + + +
+
+
+ + J + +
+
+
+
+ + J + +
+
+
+ + + + + + + +
+
+
+ + K + +
+
+
+
+ + K + +
+
+
+ + + + + + + +
+
+
+ + L + +
+
+
+
+ + L + +
+
+
+ + + + + + + +
+
+
+ + M + +
+
+
+
+ + M + +
+
+
+ + + + + + + +
+
+
+ + N + +
+
+
+
+ + N + +
+
+
+ + + + + + + +
+
+
+ + O + +
+
+
+
+ + O + +
+
+
+ + + + + + + +
+
+
+ + P + +
+
+
+
+ + P + +
+
+
+ + + + + + + +
+
+
+ + Q + +
+
+
+
+ + Q + +
+
+
+ + + + + + + +
+
+
+ + R + +
+
+
+
+ + R + +
+
+
+ + + + + + + +
+
+
+ + S + +
+
+
+
+ + S + +
+
+
+ + + + + + + +
+
+
+ + T + +
+
+
+
+ + T + +
+
+
+ + + + + + + +
+
+
+ + U + +
+
+
+
+ + U + +
+
+
+ + + + + + + +
+
+
+ + V + +
+
+
+
+ + V + +
+
+
+ + + + + + + +
+
+
+ + W + +
+
+
+
+ + W + +
+
+
+ + + + + + + +
+
+
+ + X + +
+
+
+
+ + X + +
+
+
+ + + + + + + +
+
+
+ + Y + +
+
+
+
+ + Y + +
+
+
+ + + + + + + +
+
+
+ + Z + +
+
+
+
+ + Z + +
+
+
+ + + + + + + +
+
+
+ + Dictionary + +
+
+
+
+ + Dictionary + +
+
+
+ + + + + + + +
+
+
+ + M + +
+
+
+
+ + M + +
+
+
+
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/img/tutorial/indexes/dictionary001.svg b/docs/img/tutorial/indexes/dictionary001.svg deleted file mode 100644 index b543793a25..0000000000 --- a/docs/img/tutorial/indexes/dictionary001.svg +++ /dev/null @@ -1,57 +0,0 @@ -
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
N
N
O
O
P
P
Q
Q
R
R
S
S
T
T
U
U
V
V
W
W
X
X
Y
Y
Z
Z
Dictionary
Dictionary
M
M
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/img/tutorial/indexes/dictionary002.drawio b/docs/img/tutorial/indexes/dictionary002.drawio deleted file mode 100644 index cb1857b1ad..0000000000 --- a/docs/img/tutorial/indexes/dictionary002.drawio +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/img/tutorial/indexes/dictionary002.drawio.svg b/docs/img/tutorial/indexes/dictionary002.drawio.svg new file mode 100644 index 0000000000..b468c90607 --- /dev/null +++ b/docs/img/tutorial/indexes/dictionary002.drawio.svg @@ -0,0 +1,660 @@ + + + + + + + + + + + + + +
+
+
+ + A + +
+
+
+
+ + A + +
+
+
+ + + + + + + +
+
+
+ + B + +
+
+
+
+ + B + +
+
+
+ + + + + + + +
+
+
+ + C + +
+
+
+
+ + C + +
+
+
+ + + + + + + +
+
+
+ + D + +
+
+
+
+ + D + +
+
+
+ + + + + + + +
+
+
+ + E + +
+
+
+
+ + E + +
+
+
+ + + + + + + +
+
+
+ + F + +
+
+
+
+ + F + +
+
+
+ + + + + + + +
+
+
+ + G + +
+
+
+
+ + G + +
+
+
+ + + + + + + +
+
+
+ + H + +
+
+
+
+ + H + +
+
+
+ + + + + + + +
+
+
+ + I + +
+
+
+
+ + I + +
+
+
+ + + + + + + +
+
+
+ + J + +
+
+
+
+ + J + +
+
+
+ + + + + + + +
+
+
+ + K + +
+
+
+
+ + K + +
+
+
+ + + + + + + +
+
+
+ + L + +
+
+
+
+ + L + +
+
+
+ + + + + + + +
+
+
+ + M + +
+
+
+
+ + M + +
+
+
+ + + + + + + +
+
+
+ + N + +
+
+
+
+ + N + +
+
+
+ + + + + + + +
+
+
+ + O + +
+
+
+
+ + O + +
+
+
+ + + + + + + +
+
+
+ + P + +
+
+
+
+ + P + +
+
+
+ + + + + + + +
+
+
+ + Q + +
+
+
+
+ + Q + +
+
+
+ + + + + + + +
+
+
+ + R + +
+
+
+
+ + R + +
+
+
+ + + + + + + +
+
+
+ + S + +
+
+
+
+ + S + +
+
+
+ + + + + + + +
+
+
+ + T + +
+
+
+
+ + T + +
+
+
+ + + + + + + +
+
+
+ + U + +
+
+
+
+ + U + +
+
+
+ + + + + + + +
+
+
+ + V + +
+
+
+
+ + V + +
+
+
+ + + + + + + +
+
+
+ + W + +
+
+
+
+ + W + +
+
+
+ + + + + + + +
+
+
+ + X + +
+
+
+
+ + X + +
+
+
+ + + + + + + +
+
+
+ + Y + +
+
+
+
+ + Y + +
+
+
+ + + + + + + +
+
+
+ + Z + +
+
+
+
+ + Z + +
+
+
+ + + + + + + +
+
+
+ + Dictionary + +
+
+
+
+ + Dictionary + +
+
+
+ + + + + + + +
+
+
+ + M + +
+
+
+
+ + M + +
+
+
+
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/img/tutorial/indexes/dictionary002.svg b/docs/img/tutorial/indexes/dictionary002.svg deleted file mode 100644 index 677687d248..0000000000 --- a/docs/img/tutorial/indexes/dictionary002.svg +++ /dev/null @@ -1 +0,0 @@ -
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
N
N
O
O
P
P
Q
Q
R
R
S
S
T
T
U
U
V
V
W
W
X
X
Y
Y
Z
Z
Dictionary
Dictionary
M
M
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/img/tutorial/indexes/dictionary003.drawio b/docs/img/tutorial/indexes/dictionary003.drawio deleted file mode 100644 index 845eb065cd..0000000000 --- a/docs/img/tutorial/indexes/dictionary003.drawio +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/img/tutorial/indexes/dictionary003.drawio.svg b/docs/img/tutorial/indexes/dictionary003.drawio.svg new file mode 100644 index 0000000000..2a036508f6 --- /dev/null +++ b/docs/img/tutorial/indexes/dictionary003.drawio.svg @@ -0,0 +1,640 @@ + + + + + + + + + + + + + +
+
+
+ + A + +
+
+
+
+ + A + +
+
+
+ + + + + + + +
+
+
+ + B + +
+
+
+
+ + B + +
+
+
+ + + + + + + +
+
+
+ + C + +
+
+
+
+ + C + +
+
+
+ + + + + + + +
+
+
+ + D + +
+
+
+
+ + D + +
+
+
+ + + + + + + +
+
+
+ + E + +
+
+
+
+ + E + +
+
+
+ + + + + + + +
+
+
+ + F + +
+
+
+
+ + F + +
+
+
+ + + + + + + +
+
+
+ + G + +
+
+
+
+ + G + +
+
+
+ + + + + + + +
+
+
+ + H + +
+
+
+
+ + H + +
+
+
+ + + + + + + +
+
+
+ + I + +
+
+
+
+ + I + +
+
+
+ + + + + + + +
+
+
+ + J + +
+
+
+
+ + J + +
+
+
+ + + + + + + +
+
+
+ + K + +
+
+
+
+ + K + +
+
+
+ + + + + + + +
+
+
+ + L + +
+
+
+
+ + L + +
+
+
+ + + + + + + +
+
+
+ + M + +
+
+
+
+ + M + +
+
+
+ + + + + + + +
+
+
+ + N + +
+
+
+
+ + N + +
+
+
+ + + + + + + +
+
+
+ + O + +
+
+
+
+ + O + +
+
+
+ + + + + + + +
+
+
+ + P + +
+
+
+
+ + P + +
+
+
+ + + + + + + +
+
+
+ + Q + +
+
+
+
+ + Q + +
+
+
+ + + + + + + +
+
+
+ + R + +
+
+
+
+ + R + +
+
+
+ + + + + + + +
+
+
+ + S + +
+
+
+
+ + S + +
+
+
+ + + + + + + +
+
+
+ + T + +
+
+
+
+ + T + +
+
+
+ + + + + + + +
+
+
+ + U + +
+
+
+
+ + U + +
+
+
+ + + + + + + +
+
+
+ + V + +
+
+
+
+ + V + +
+
+
+ + + + + + + +
+
+
+ + W + +
+
+
+
+ + W + +
+
+
+ + + + + + + +
+
+
+ + X + +
+
+
+
+ + X + +
+
+
+ + + + + + + +
+
+
+ + Y + +
+
+
+
+ + Y + +
+
+
+ + + + + + + +
+
+
+ + Z + +
+
+
+
+ + Z + +
+
+
+ + + + + + + +
+
+
+ + Dictionary + +
+
+
+
+ + Dictionary + +
+
+
+ + + +
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/img/tutorial/indexes/dictionary003.svg b/docs/img/tutorial/indexes/dictionary003.svg deleted file mode 100644 index d667a68893..0000000000 --- a/docs/img/tutorial/indexes/dictionary003.svg +++ /dev/null @@ -1 +0,0 @@ -
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
N
N
O
O
P
P
Q
Q
R
R
S
S
T
T
U
U
V
V
W
W
X
X
Y
Y
Z
Z
Dictionary
Dictionary
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/img/tutorial/indexes/dictionary004.drawio b/docs/img/tutorial/indexes/dictionary004.drawio deleted file mode 100644 index 14bbb1e26e..0000000000 --- a/docs/img/tutorial/indexes/dictionary004.drawio +++ /dev/null @@ -1,100 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/img/tutorial/indexes/dictionary004.drawio.svg b/docs/img/tutorial/indexes/dictionary004.drawio.svg new file mode 100644 index 0000000000..76974a4bfb --- /dev/null +++ b/docs/img/tutorial/indexes/dictionary004.drawio.svg @@ -0,0 +1,663 @@ + + + + + + + + + + + + + +
+
+
+ + A + +
+
+
+
+ + A + +
+
+
+ + + + + + + +
+
+
+ + B + +
+
+
+
+ + B + +
+
+
+ + + + + + + +
+
+
+ + C + +
+
+
+
+ + C + +
+
+
+ + + + + + + +
+
+
+ + D + +
+
+
+
+ + D + +
+
+
+ + + + + + + +
+
+
+ + E + +
+
+
+
+ + E + +
+
+
+ + + + + + + +
+
+
+ + F + +
+
+
+
+ + F + +
+
+
+ + + + + + + +
+
+
+ + G + +
+
+
+
+ + G + +
+
+
+ + + + + + + +
+
+
+ + H + +
+
+
+
+ + H + +
+
+
+ + + + + + + +
+
+
+ + I + +
+
+
+
+ + I + +
+
+
+ + + + + + + +
+
+
+ + J + +
+
+
+
+ + J + +
+
+
+ + + + + + + +
+
+
+ + K + +
+
+
+
+ + K + +
+
+
+ + + + + + + +
+
+
+ + L + +
+
+
+
+ + L + +
+
+
+ + + + + + + +
+
+
+ + M + +
+
+
+
+ + M + +
+
+
+ + + + + + + +
+
+
+ + N + +
+
+
+
+ + N + +
+
+
+ + + + + + + +
+
+
+ + O + +
+
+
+
+ + O + +
+
+
+ + + + + + + +
+
+
+ + P + +
+
+
+
+ + P + +
+
+
+ + + + + + + +
+
+
+ + Q + +
+
+
+
+ + Q + +
+
+
+ + + + + + + +
+
+
+ + R + +
+
+
+
+ + R + +
+
+
+ + + + + + + +
+
+
+ + S + +
+
+
+
+ + S + +
+
+
+ + + + + + + +
+
+
+ + T + +
+
+
+
+ + T + +
+
+
+ + + + + + + +
+
+
+ + U + +
+
+
+
+ + U + +
+
+
+ + + + + + + +
+
+
+ + V + +
+
+
+
+ + V + +
+
+
+ + + + + + + +
+
+
+ + W + +
+
+
+
+ + W + +
+
+
+ + + + + + + +
+
+
+ + X + +
+
+
+
+ + X + +
+
+
+ + + + + + + +
+
+
+ + Y + +
+
+
+
+ + Y + +
+
+
+ + + + + + + +
+
+
+ + Z + +
+
+
+
+ + Z + +
+
+
+ + + + + + + +
+
+
+ + Dictionary + +
+
+
+
+ + Dictionary + +
+
+
+ + + + + + + +
+
+
+ + F + +
+
+
+
+ + F + +
+
+
+ + + +
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/img/tutorial/indexes/dictionary004.svg b/docs/img/tutorial/indexes/dictionary004.svg deleted file mode 100644 index f881d6c9c2..0000000000 --- a/docs/img/tutorial/indexes/dictionary004.svg +++ /dev/null @@ -1 +0,0 @@ -
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
N
N
O
O
P
P
Q
Q
R
R
S
S
T
T
U
U
V
V
W
W
X
X
Y
Y
Z
Z
Dictionary
Dictionary
F
F
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/img/tutorial/indexes/dictionary005.drawio b/docs/img/tutorial/indexes/dictionary005.drawio deleted file mode 100644 index 9e339c177e..0000000000 --- a/docs/img/tutorial/indexes/dictionary005.drawio +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/img/tutorial/indexes/dictionary005.drawio.svg b/docs/img/tutorial/indexes/dictionary005.drawio.svg new file mode 100644 index 0000000000..865ad3530f --- /dev/null +++ b/docs/img/tutorial/indexes/dictionary005.drawio.svg @@ -0,0 +1,640 @@ + + + + + + + + + + + + + +
+
+
+ + A + +
+
+
+
+ + A + +
+
+
+ + + + + + + +
+
+
+ + B + +
+
+
+
+ + B + +
+
+
+ + + + + + + +
+
+
+ + C + +
+
+
+
+ + C + +
+
+
+ + + + + + + +
+
+
+ + D + +
+
+
+
+ + D + +
+
+
+ + + + + + + +
+
+
+ + E + +
+
+
+
+ + E + +
+
+
+ + + + + + + +
+
+
+ + F + +
+
+
+
+ + F + +
+
+
+ + + + + + + +
+
+
+ + G + +
+
+
+
+ + G + +
+
+
+ + + + + + + +
+
+
+ + H + +
+
+
+
+ + H + +
+
+
+ + + + + + + +
+
+
+ + I + +
+
+
+
+ + I + +
+
+
+ + + + + + + +
+
+
+ + J + +
+
+
+
+ + J + +
+
+
+ + + + + + + +
+
+
+ + K + +
+
+
+
+ + K + +
+
+
+ + + + + + + +
+
+
+ + L + +
+
+
+
+ + L + +
+
+
+ + + + + + + +
+
+
+ + M + +
+
+
+
+ + M + +
+
+
+ + + + + + + +
+
+
+ + N + +
+
+
+
+ + N + +
+
+
+ + + + + + + +
+
+
+ + O + +
+
+
+
+ + O + +
+
+
+ + + + + + + +
+
+
+ + P + +
+
+
+
+ + P + +
+
+
+ + + + + + + +
+
+
+ + Q + +
+
+
+
+ + Q + +
+
+
+ + + + + + + +
+
+
+ + R + +
+
+
+
+ + R + +
+
+
+ + + + + + + +
+
+
+ + S + +
+
+
+
+ + S + +
+
+
+ + + + + + + +
+
+
+ + T + +
+
+
+
+ + T + +
+
+
+ + + + + + + +
+
+
+ + U + +
+
+
+
+ + U + +
+
+
+ + + + + + + +
+
+
+ + V + +
+
+
+
+ + V + +
+
+
+ + + + + + + +
+
+
+ + W + +
+
+
+
+ + W + +
+
+
+ + + + + + + +
+
+
+ + X + +
+
+
+
+ + X + +
+
+
+ + + + + + + +
+
+
+ + Y + +
+
+
+
+ + Y + +
+
+
+ + + + + + + +
+
+
+ + Z + +
+
+
+
+ + Z + +
+
+
+ + + + + + + +
+
+
+ + Dictionary + +
+
+
+
+ + Dictionary + +
+
+
+ + + +
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/img/tutorial/indexes/dictionary005.svg b/docs/img/tutorial/indexes/dictionary005.svg deleted file mode 100644 index 9d376245c0..0000000000 --- a/docs/img/tutorial/indexes/dictionary005.svg +++ /dev/null @@ -1 +0,0 @@ -
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
N
N
O
O
P
P
Q
Q
R
R
S
S
T
T
U
U
V
V
W
W
X
X
Y
Y
Z
Z
Dictionary
Dictionary
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/img/tutorial/indexes/dictionary006.drawio b/docs/img/tutorial/indexes/dictionary006.drawio deleted file mode 100644 index 3c669d323f..0000000000 --- a/docs/img/tutorial/indexes/dictionary006.drawio +++ /dev/null @@ -1,100 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/img/tutorial/indexes/dictionary006.drawio.svg b/docs/img/tutorial/indexes/dictionary006.drawio.svg new file mode 100644 index 0000000000..2947ee7db9 --- /dev/null +++ b/docs/img/tutorial/indexes/dictionary006.drawio.svg @@ -0,0 +1,663 @@ + + + + + + + + + + + + + +
+
+
+ + A + +
+
+
+
+ + A + +
+
+
+ + + + + + + +
+
+
+ + B + +
+
+
+
+ + B + +
+
+
+ + + + + + + +
+
+
+ + C + +
+
+
+
+ + C + +
+
+
+ + + + + + + +
+
+
+ + D + +
+
+
+
+ + D + +
+
+
+ + + + + + + +
+
+
+ + E + +
+
+
+
+ + E + +
+
+
+ + + + + + + +
+
+
+ + F + +
+
+
+
+ + F + +
+
+
+ + + + + + + +
+
+
+ + G + +
+
+
+
+ + G + +
+
+
+ + + + + + + +
+
+
+ + H + +
+
+
+
+ + H + +
+
+
+ + + + + + + +
+
+
+ + I + +
+
+
+
+ + I + +
+
+
+ + + + + + + +
+
+
+ + J + +
+
+
+
+ + J + +
+
+
+ + + + + + + +
+
+
+ + K + +
+
+
+
+ + K + +
+
+
+ + + + + + + +
+
+
+ + L + +
+
+
+
+ + L + +
+
+
+ + + + + + + +
+
+
+ + M + +
+
+
+
+ + M + +
+
+
+ + + + + + + +
+
+
+ + N + +
+
+
+
+ + N + +
+
+
+ + + + + + + +
+
+
+ + O + +
+
+
+
+ + O + +
+
+
+ + + + + + + +
+
+
+ + P + +
+
+
+
+ + P + +
+
+
+ + + + + + + +
+
+
+ + Q + +
+
+
+
+ + Q + +
+
+
+ + + + + + + +
+
+
+ + R + +
+
+
+
+ + R + +
+
+
+ + + + + + + +
+
+
+ + S + +
+
+
+
+ + S + +
+
+
+ + + + + + + +
+
+
+ + T + +
+
+
+
+ + T + +
+
+
+ + + + + + + +
+
+
+ + U + +
+
+
+
+ + U + +
+
+
+ + + + + + + +
+
+
+ + V + +
+
+
+
+ + V + +
+
+
+ + + + + + + +
+
+
+ + W + +
+
+
+
+ + W + +
+
+
+ + + + + + + +
+
+
+ + X + +
+
+
+
+ + X + +
+
+
+ + + + + + + +
+
+
+ + Y + +
+
+
+
+ + Y + +
+
+
+ + + + + + + +
+
+
+ + Z + +
+
+
+
+ + Z + +
+
+
+ + + + + + + +
+
+
+ + Dictionary + +
+
+
+
+ + Dictionary + +
+
+
+ + + + + + + + + + +
+
+
+ + C + +
+
+
+
+ + C + +
+
+
+
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/img/tutorial/indexes/dictionary006.svg b/docs/img/tutorial/indexes/dictionary006.svg deleted file mode 100644 index 30be80ea8b..0000000000 --- a/docs/img/tutorial/indexes/dictionary006.svg +++ /dev/null @@ -1 +0,0 @@ -
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
N
N
O
O
P
P
Q
Q
R
R
S
S
T
T
U
U
V
V
W
W
X
X
Y
Y
Z
Z
Dictionary
Dictionary
C
C
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/img/tutorial/indexes/dictionary007.drawio b/docs/img/tutorial/indexes/dictionary007.drawio deleted file mode 100644 index 89b32cabaf..0000000000 --- a/docs/img/tutorial/indexes/dictionary007.drawio +++ /dev/null @@ -1,100 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/img/tutorial/indexes/dictionary007.drawio.svg b/docs/img/tutorial/indexes/dictionary007.drawio.svg new file mode 100644 index 0000000000..651ad3287c --- /dev/null +++ b/docs/img/tutorial/indexes/dictionary007.drawio.svg @@ -0,0 +1,643 @@ + + + + + + + + + + + + + +
+
+
+ + A + +
+
+
+
+ + A + +
+
+
+ + + + + + + +
+
+
+ + B + +
+
+
+
+ + B + +
+
+
+ + + + + + + +
+
+
+ + C + +
+
+
+
+ + C + +
+
+
+ + + + + + + +
+
+
+ + D + +
+
+
+
+ + D + +
+
+
+ + + + + + + +
+
+
+ + E + +
+
+
+
+ + E + +
+
+
+ + + + + + + +
+
+
+ + F + +
+
+
+
+ + F + +
+
+
+ + + + + + + +
+
+
+ + G + +
+
+
+
+ + G + +
+
+
+ + + + + + + +
+
+
+ + H + +
+
+
+
+ + H + +
+
+
+ + + + + + + +
+
+
+ + I + +
+
+
+
+ + I + +
+
+
+ + + + + + + +
+
+
+ + J + +
+
+
+
+ + J + +
+
+
+ + + + + + + +
+
+
+ + K + +
+
+
+
+ + K + +
+
+
+ + + + + + + +
+
+
+ + L + +
+
+
+
+ + L + +
+
+
+ + + + + + + +
+
+
+ + M + +
+
+
+
+ + M + +
+
+
+ + + + + + + +
+
+
+ + N + +
+
+
+
+ + N + +
+
+
+ + + + + + + +
+
+
+ + O + +
+
+
+
+ + O + +
+
+
+ + + + + + + +
+
+
+ + P + +
+
+
+
+ + P + +
+
+
+ + + + + + + +
+
+
+ + Q + +
+
+
+
+ + Q + +
+
+
+ + + + + + + +
+
+
+ + R + +
+
+
+
+ + R + +
+
+
+ + + + + + + +
+
+
+ + S + +
+
+
+
+ + S + +
+
+
+ + + + + + + +
+
+
+ + T + +
+
+
+
+ + T + +
+
+
+ + + + + + + +
+
+
+ + U + +
+
+
+
+ + U + +
+
+
+ + + + + + + +
+
+
+ + V + +
+
+
+
+ + V + +
+
+
+ + + + + + + +
+
+
+ + W + +
+
+
+
+ + W + +
+
+
+ + + + + + + +
+
+
+ + X + +
+
+
+
+ + X + +
+
+
+ + + + + + + +
+
+
+ + Y + +
+
+
+
+ + Y + +
+
+
+ + + + + + + +
+
+
+ + Z + +
+
+
+
+ + Z + +
+
+
+ + + + + + + +
+
+
+ + Dictionary + +
+
+
+
+ + Dictionary + +
+
+
+ + + + + + +
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/img/tutorial/indexes/dictionary007.svg b/docs/img/tutorial/indexes/dictionary007.svg deleted file mode 100644 index 79e795060e..0000000000 --- a/docs/img/tutorial/indexes/dictionary007.svg +++ /dev/null @@ -1 +0,0 @@ -
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
N
N
O
O
P
P
Q
Q
R
R
S
S
T
T
U
U
V
V
W
W
X
X
Y
Y
Z
Z
Dictionary
Dictionary
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/img/tutorial/indexes/dictionary008.drawio b/docs/img/tutorial/indexes/dictionary008.drawio deleted file mode 100644 index ac08ad04d4..0000000000 --- a/docs/img/tutorial/indexes/dictionary008.drawio +++ /dev/null @@ -1,103 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/img/tutorial/indexes/dictionary008.drawio.svg b/docs/img/tutorial/indexes/dictionary008.drawio.svg new file mode 100644 index 0000000000..611c33177b --- /dev/null +++ b/docs/img/tutorial/indexes/dictionary008.drawio.svg @@ -0,0 +1,666 @@ + + + + + + + + + + + + + +
+
+
+ + A + +
+
+
+
+ + A + +
+
+
+ + + + + + + +
+
+
+ + B + +
+
+
+
+ + B + +
+
+
+ + + + + + + +
+
+
+ + C + +
+
+
+
+ + C + +
+
+
+ + + + + + + +
+
+
+ + D + +
+
+
+
+ + D + +
+
+
+ + + + + + + +
+
+
+ + E + +
+
+
+
+ + E + +
+
+
+ + + + + + + +
+
+
+ + F + +
+
+
+
+ + F + +
+
+
+ + + + + + + +
+
+
+ + G + +
+
+
+
+ + G + +
+
+
+ + + + + + + +
+
+
+ + H + +
+
+
+
+ + H + +
+
+
+ + + + + + + +
+
+
+ + I + +
+
+
+
+ + I + +
+
+
+ + + + + + + +
+
+
+ + J + +
+
+
+
+ + J + +
+
+
+ + + + + + + +
+
+
+ + K + +
+
+
+
+ + K + +
+
+
+ + + + + + + +
+
+
+ + L + +
+
+
+
+ + L + +
+
+
+ + + + + + + +
+
+
+ + M + +
+
+
+
+ + M + +
+
+
+ + + + + + + +
+
+
+ + N + +
+
+
+
+ + N + +
+
+
+ + + + + + + +
+
+
+ + O + +
+
+
+
+ + O + +
+
+
+ + + + + + + +
+
+
+ + P + +
+
+
+
+ + P + +
+
+
+ + + + + + + +
+
+
+ + Q + +
+
+
+
+ + Q + +
+
+
+ + + + + + + +
+
+
+ + R + +
+
+
+
+ + R + +
+
+
+ + + + + + + +
+
+
+ + S + +
+
+
+
+ + S + +
+
+
+ + + + + + + +
+
+
+ + T + +
+
+
+
+ + T + +
+
+
+ + + + + + + +
+
+
+ + U + +
+
+
+
+ + U + +
+
+
+ + + + + + + +
+
+
+ + V + +
+
+
+
+ + V + +
+
+
+ + + + + + + +
+
+
+ + W + +
+
+
+
+ + W + +
+
+
+ + + + + + + +
+
+
+ + X + +
+
+
+
+ + X + +
+
+
+ + + + + + + +
+
+
+ + Y + +
+
+
+
+ + Y + +
+
+
+ + + + + + + +
+
+
+ + Z + +
+
+
+
+ + Z + +
+
+
+ + + + + + + +
+
+
+ + Dictionary + +
+
+
+
+ + Dictionary + +
+
+
+ + + + + + + + + + + + + +
+
+
+ + D + +
+
+
+
+ + D + +
+
+
+
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/img/tutorial/indexes/dictionary008.svg b/docs/img/tutorial/indexes/dictionary008.svg deleted file mode 100644 index 013a4d64a3..0000000000 --- a/docs/img/tutorial/indexes/dictionary008.svg +++ /dev/null @@ -1 +0,0 @@ -
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
N
N
O
O
P
P
Q
Q
R
R
S
S
T
T
U
U
V
V
W
W
X
X
Y
Y
Z
Z
Dictionary
Dictionary
D
D
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/img/tutorial/indexes/techbook001.drawio b/docs/img/tutorial/indexes/techbook001.drawio deleted file mode 100644 index de1c25668c..0000000000 --- a/docs/img/tutorial/indexes/techbook001.drawio +++ /dev/null @@ -1,92 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/img/tutorial/indexes/techbook001.drawio.svg b/docs/img/tutorial/indexes/techbook001.drawio.svg new file mode 100644 index 0000000000..cb022465f6 --- /dev/null +++ b/docs/img/tutorial/indexes/techbook001.drawio.svg @@ -0,0 +1,346 @@ + + + + + + + + + + + + + +
+
+
+ + Technical Book + +
+
+
+
+ + Technical Book + +
+
+
+ + + + + + + +
+
+
+ + Chapter 1 + +
+
+
+
+ + Chapter 1 + +
+
+
+ + + + + + + +
+
+
+ + Chapter 2 + +
+
+
+
+ + Chapter 2 + +
+
+
+ + + + + + + +
+
+
+ + Chapter 3 + +
+
+
+
+ + Chapter 3 + +
+
+
+ + + + + + + +
+
+
+ + Chapter 4 + +
+
+
+
+ + Chapter 4 + +
+
+
+ + + + + + + +
+
+
+ + Chapter 5 + +
+
+
+
+ + Chapter 5 + +
+
+
+ + + + + + + +
+
+
+ + Chapter 6 + +
+
+
+
+ + Chapter 6 + +
+
+
+ + + + + + + +
+
+
+ + Chapter 7 + +
+
+
+
+ + Chapter 7 + +
+
+
+ + + + + + + + + + +
+
+
+ + Book Index + +
+
+
+
+ + Book Index + +
+
+
+ + + + + + + + + + + + + + + +
+
+
+ + Database + +
+
+
+
+ + Database + +
+
+
+ + + + + + + + + + + + + + + +
+
+
+ + Python + +
+
+
+
+ + Python + +
+
+
+ + + + + + + + + + + + + + + +
+
+
+ + Files + +
+
+
+
+ + Files + +
+
+
+ + + + + + + + + + + +
+
+
+ + Editors + +
+
+
+
+ + Editors + +
+
+
+
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/img/tutorial/indexes/techbook001.svg b/docs/img/tutorial/indexes/techbook001.svg deleted file mode 100644 index 8b0c09ddcf..0000000000 --- a/docs/img/tutorial/indexes/techbook001.svg +++ /dev/null @@ -1 +0,0 @@ -
Technical Book
Technical Book
Chapter 1
Chapter 1
Chapter 2
Chapter 2
Chapter 3
Chapter 3
Chapter 4
Chapter 4
Chapter 5
Chapter 5
Chapter 6
Chapter 6
Chapter 7
Chapter 7
Book Index
Book Index
Database
Database
Python
Python
Files
Files
Editors
Editors
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/img/tutorial/many-to-many/many-to-many.drawio b/docs/img/tutorial/many-to-many/many-to-many.drawio deleted file mode 100644 index d585e5e0e6..0000000000 --- a/docs/img/tutorial/many-to-many/many-to-many.drawio +++ /dev/null @@ -1,220 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/img/tutorial/many-to-many/many-to-many.drawio.svg b/docs/img/tutorial/many-to-many/many-to-many.drawio.svg new file mode 100644 index 0000000000..d362455cec --- /dev/null +++ b/docs/img/tutorial/many-to-many/many-to-many.drawio.svg @@ -0,0 +1,1206 @@ + + + + + + + + + + + + + + + + + + + + +
+
+
+ + hero + +
+
+
+
+ + hero + +
+
+
+ + + + + + + + + + + +
+
+
+ + + id + + +
+
+
+
+ + id + +
+
+
+ + + + + + + + +
+
+
+ + + name + + +
+
+
+
+ + name + +
+
+
+ + + + + + + + +
+
+
+ + + secret_name + + +
+
+
+
+ + secret_name + +
+
+
+ + + + + + + + +
+
+
+ + + age + + +
+
+
+
+ + age + +
+
+
+ + + + + + + + + + + +
+
+
+ + + 1 + + +
+
+
+
+ + 1 + +
+
+
+ + + + + + + + +
+
+
+ + Deadpond + +
+
+
+
+ + Deadpond + +
+
+
+ + + + + + + + +
+
+
+ + Dive Wilson + +
+
+
+
+ + Dive Wilson + +
+
+
+ + + + + + + + +
+
+
+ + null + +
+
+
+
+ + null + +
+
+
+ + + + + + + + + + + +
+
+
+ + 2 + +
+
+
+
+ + 2 + +
+
+
+ + + + + + + + +
+
+
+ + Spider-Boy + +
+
+
+
+ + Spider-Boy + +
+
+
+ + + + + + + + +
+
+
+ + Pedro Parqueador + +
+
+
+
+ + Pedro Parqueador + +
+
+
+ + + + + + + + +
+
+
+ + null + +
+
+
+
+ + null + +
+
+
+ + + + + + + + + + + +
+
+
+ + 3 + +
+
+
+
+ + 3 + +
+
+
+ + + + + + + + +
+
+
+ + Rusty-Man + +
+
+
+
+ + Rusty-Man + +
+
+
+ + + + + + + + +
+
+
+ + Tommy Sharp + +
+
+
+
+ + Tommy Sharp + +
+
+
+ + + + + + + + +
+
+
+ + 48 + +
+
+
+
+ + 48 + +
+
+
+ + + + + + + + + + + + + +
+
+
+ + team + +
+
+
+
+ + team + +
+
+
+ + + + + + + + + + + +
+
+
+ + + id + + +
+
+
+
+ + id + +
+
+
+ + + + + + + + +
+
+
+ + + name + + +
+
+
+
+ + name + +
+
+
+ + + + + + + + +
+
+
+ + + headquarters + + +
+
+
+
+ + headquarters + +
+
+
+ + + + + + + + + + + +
+
+
+ + + 1 + + +
+
+
+
+ + 1 + +
+
+
+ + + + + + + + +
+
+
+ + Preventers + +
+
+
+
+ + Preventers + +
+
+
+ + + + + + + + +
+
+
+ + + Sharp Tower + + +
+
+
+
+ + Sharp Tower + +
+
+
+ + + + + + + + + + + +
+
+
+ + 2 + +
+
+
+
+ + 2 + +
+
+
+ + + + + + + + +
+
+
+ + Z-Force + +
+
+
+
+ + Z-Force + +
+
+
+ + + + + + + + +
+
+
+

+ + Sister Margaret's Bar + +

+
+
+
+
+ + Sister Margaret's Bar + +
+
+
+ + + + + + + + + + + + + + +
+
+
+ + heroteamlink + +
+
+
+
+ + heroteamlink + +
+
+
+ + + + + + + + + + + +
+
+
+ + + hero_id + + +
+
+
+
+ + hero_id + +
+
+
+ + + + + + + + +
+
+
+ + + team_id + + +
+
+
+
+ + team_id + +
+
+
+ + + + + + + + + + + +
+
+
+ + + 1 + + +
+
+
+
+ + 1 + +
+
+
+ + + + + + + + +
+
+
+ + 1 + +
+
+
+
+ + 1 + +
+
+
+ + + + + + + + + + + +
+
+
+ + 1 + +
+
+
+
+ + 1 + +
+
+
+ + + + + + + + +
+
+
+ + 2 + +
+
+
+
+ + 2 + +
+
+
+ + + + + + + + + + + +
+
+
+ + 2 + +
+
+
+
+ + 2 + +
+
+
+ + + + + + + + +
+
+
+ + 1 + +
+
+
+
+ + 1 + +
+
+
+ + + + + + + + + + + +
+
+
+ + 3 + +
+
+
+
+ + 3 + +
+
+
+ + + + + + + + +
+
+
+ + + 1 + + +
+
+
+
+ + 1 + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/img/tutorial/many-to-many/many-to-many.svg b/docs/img/tutorial/many-to-many/many-to-many.svg deleted file mode 100644 index d35c280352..0000000000 --- a/docs/img/tutorial/many-to-many/many-to-many.svg +++ /dev/null @@ -1,57 +0,0 @@ -
hero
hero
id
id
name
name
secret_name
secret_name
age
age
1
1
Deadpond
Deadpond
Dive Wilson
Dive Wilson
null
null
2
2
Spider-Boy
Spider-Boy
Pedro Parqueador
Pedro Parqueador
null
null
3
3
Rusty-Man
Rusty-Man
Tommy Sharp
Tommy Sharp
48
48
team
team
id
id
name
name
headquarters
headquarters
1
1
Preventers
Preventers
Sharp Tower
Sharp Tower
2
2
Z-Force
Z-Force

Sister Margaret’s Bar

Sister Margaret’s Bar
heroteamlink
heroteamlink
hero_id
hero_id
team_id
team_id
1
1
1
1
1
1
2
2
2
2
1
1
3
3
1
1
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/img/tutorial/offset-and-limit/limit.drawio b/docs/img/tutorial/offset-and-limit/limit.drawio deleted file mode 100644 index 569dfc5437..0000000000 --- a/docs/img/tutorial/offset-and-limit/limit.drawio +++ /dev/null @@ -1,133 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/img/tutorial/offset-and-limit/limit.drawio.svg b/docs/img/tutorial/offset-and-limit/limit.drawio.svg new file mode 100644 index 0000000000..41ec69a3cd --- /dev/null +++ b/docs/img/tutorial/offset-and-limit/limit.drawio.svg @@ -0,0 +1,1031 @@ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + hero + +
+
+
+
+ + hero + +
+
+
+ + + + + + + + + + + +
+
+
+ + + id + + +
+
+
+
+ + id + +
+
+
+ + + + + + + + +
+
+
+ + + name + + +
+
+
+
+ + name + +
+
+
+ + + + + + + + +
+
+
+ + + secret_name + + +
+
+
+
+ + secret_name + +
+
+
+ + + + + + + + +
+
+
+ + + age + + +
+
+
+
+ + age + +
+
+
+ + + + + + + + + + + +
+
+
+ + + 1 + + +
+
+
+
+ + 1 + +
+
+
+ + + + + + + + +
+
+
+ + Deadpond + +
+
+
+
+ + Deadpond + +
+
+
+ + + + + + + + +
+
+
+ + Dive Wilson + +
+
+
+
+ + Dive Wilson + +
+
+
+ + + + + + + + +
+
+
+ + null + +
+
+
+
+ + null + +
+
+
+ + + + + + + + + + + +
+
+
+ + 2 + +
+
+
+
+ + 2 + +
+
+
+ + + + + + + + +
+
+
+ + Spider-Boy + +
+
+
+
+ + Spider-Boy + +
+
+
+ + + + + + + + +
+
+
+ + Pedro Parqueador + +
+
+
+
+ + Pedro Parqueador + +
+
+
+ + + + + + + + +
+
+
+ + null + +
+
+
+
+ + null + +
+
+
+ + + + + + + + + + + +
+
+
+ + 3 + +
+
+
+
+ + 3 + +
+
+
+ + + + + + + + +
+
+
+ + Rusty-Man + +
+
+
+
+ + Rusty-Man + +
+
+
+ + + + + + + + +
+
+
+ + Tommy Sharp + +
+
+
+
+ + Tommy Sharp + +
+
+
+ + + + + + + + +
+
+
+ + 48 + +
+
+
+
+ + 48 + +
+
+
+ + + + + + + + + + + +
+
+
+ + + 4 + + +
+
+
+
+ + 4 + +
+
+
+ + + + + + + + +
+
+
+ + Tarantula + +
+
+
+
+ + Tarantula + +
+
+
+ + + + + + + + +
+
+
+ + + Natalia Roman-on + + +
+
+
+
+ + Natalia Roman-on + +
+
+
+ + + + + + + + +
+
+
+ + 32 + +
+
+
+
+ + 32 + +
+
+
+ + + + + + + + + + + +
+
+
+ + 5 + +
+
+
+
+ + 5 + +
+
+
+ + + + + + + + +
+
+
+ + + Black Lion + + +
+
+
+
+ + Black Lion + +
+
+
+ + + + + + + + +
+
+
+ + + Trevor Challa + + +
+
+
+
+ + Trevor Challa + +
+
+
+ + + + + + + + +
+
+
+ + 35 + +
+
+
+
+ + 35 + +
+
+
+ + + + + + + + + + + +
+
+
+ + 6 + +
+
+
+
+ + 6 + +
+
+
+ + + + + + + + +
+
+
+ + + Dr. Weird + + +
+
+
+
+ + Dr. Weird + +
+
+
+ + + + + + + + +
+
+
+ + + Steve Weird + + +
+
+
+
+ + Steve Weird + +
+
+
+ + + + + + + + +
+
+
+ + 36 + +
+
+
+
+ + 36 + +
+
+
+ + + + + + + + + + + +
+
+
+ + 7 + +
+
+
+
+ + 7 + +
+
+
+ + + + + + + + +
+
+
+ + + Captain North America + + +
+
+
+
+ + Captain North America + +
+
+
+ + + + + + + + +
+
+
+ + + Esteban Rogelios + + +
+
+
+
+ + Esteban Rogelios + +
+
+
+ + + + + + + + +
+
+
+ + 93 + +
+
+
+
+ + 93 + +
+
+
+
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/img/tutorial/offset-and-limit/limit.svg b/docs/img/tutorial/offset-and-limit/limit.svg deleted file mode 100644 index 3e31086a10..0000000000 --- a/docs/img/tutorial/offset-and-limit/limit.svg +++ /dev/null @@ -1,57 +0,0 @@ -
hero
hero
id
id
name
name
secret_name
secret_name
age
age
1
1
Deadpond
Deadpond
Dive Wilson
Dive Wilson
null
null
2
2
Spider-Boy
Spider-Boy
Pedro Parqueador
Pedro Parqueador
null
null
3
3
Rusty-Man
Rusty-Man
Tommy Sharp
Tommy Sharp
48
48
4
4
Tarantula
Tarantula
Natalia Roman-on
Natalia Roman-on
32
32
5
5
Black Lion
Black Lion
Trevor Challa
Trevor Challa
35
35
6
6
Dr. Weird
Dr. Weird
Steve Weird
Steve Weird
36
36
7
7
Captain North America
Captain North America
Esteban Rogelios
Esteban Rogelios
93
93
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/img/tutorial/offset-and-limit/limit2.drawio b/docs/img/tutorial/offset-and-limit/limit2.drawio deleted file mode 100644 index a329235f71..0000000000 --- a/docs/img/tutorial/offset-and-limit/limit2.drawio +++ /dev/null @@ -1,133 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/img/tutorial/offset-and-limit/limit2.drawio.svg b/docs/img/tutorial/offset-and-limit/limit2.drawio.svg new file mode 100644 index 0000000000..1f23e8682b --- /dev/null +++ b/docs/img/tutorial/offset-and-limit/limit2.drawio.svg @@ -0,0 +1,1031 @@ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + hero + +
+
+
+
+ + hero + +
+
+
+ + + + + + + + + + + +
+
+
+ + + id + + +
+
+
+
+ + id + +
+
+
+ + + + + + + + +
+
+
+ + + name + + +
+
+
+
+ + name + +
+
+
+ + + + + + + + +
+
+
+ + + secret_name + + +
+
+
+
+ + secret_name + +
+
+
+ + + + + + + + +
+
+
+ + + age + + +
+
+
+
+ + age + +
+
+
+ + + + + + + + + + + +
+
+
+ + + 1 + + +
+
+
+
+ + 1 + +
+
+
+ + + + + + + + +
+
+
+ + Deadpond + +
+
+
+
+ + Deadpond + +
+
+
+ + + + + + + + +
+
+
+ + Dive Wilson + +
+
+
+
+ + Dive Wilson + +
+
+
+ + + + + + + + +
+
+
+ + null + +
+
+
+
+ + null + +
+
+
+ + + + + + + + + + + +
+
+
+ + 2 + +
+
+
+
+ + 2 + +
+
+
+ + + + + + + + +
+
+
+ + Spider-Boy + +
+
+
+
+ + Spider-Boy + +
+
+
+ + + + + + + + +
+
+
+ + Pedro Parqueador + +
+
+
+
+ + Pedro Parqueador + +
+
+
+ + + + + + + + +
+
+
+ + null + +
+
+
+
+ + null + +
+
+
+ + + + + + + + + + + +
+
+
+ + 3 + +
+
+
+
+ + 3 + +
+
+
+ + + + + + + + +
+
+
+ + Rusty-Man + +
+
+
+
+ + Rusty-Man + +
+
+
+ + + + + + + + +
+
+
+ + Tommy Sharp + +
+
+
+
+ + Tommy Sharp + +
+
+
+ + + + + + + + +
+
+
+ + 48 + +
+
+
+
+ + 48 + +
+
+
+ + + + + + + + + + + +
+
+
+ + + 4 + + +
+
+
+
+ + 4 + +
+
+
+ + + + + + + + +
+
+
+ + Tarantula + +
+
+
+
+ + Tarantula + +
+
+
+ + + + + + + + +
+
+
+ + + Natalia Roman-on + + +
+
+
+
+ + Natalia Roman-on + +
+
+
+ + + + + + + + +
+
+
+ + 32 + +
+
+
+
+ + 32 + +
+
+
+ + + + + + + + + + + +
+
+
+ + 5 + +
+
+
+
+ + 5 + +
+
+
+ + + + + + + + +
+
+
+ + + Black Lion + + +
+
+
+
+ + Black Lion + +
+
+
+ + + + + + + + +
+
+
+ + + Trevor Challa + + +
+
+
+
+ + Trevor Challa + +
+
+
+ + + + + + + + +
+
+
+ + 35 + +
+
+
+
+ + 35 + +
+
+
+ + + + + + + + + + + +
+
+
+ + 6 + +
+
+
+
+ + 6 + +
+
+
+ + + + + + + + +
+
+
+ + + Dr. Weird + + +
+
+
+
+ + Dr. Weird + +
+
+
+ + + + + + + + +
+
+
+ + + Steve Weird + + +
+
+
+
+ + Steve Weird + +
+
+
+ + + + + + + + +
+
+
+ + 36 + +
+
+
+
+ + 36 + +
+
+
+ + + + + + + + + + + +
+
+
+ + 7 + +
+
+
+
+ + 7 + +
+
+
+ + + + + + + + +
+
+
+ + + Captain North America + + +
+
+
+
+ + Captain North America + +
+
+
+ + + + + + + + +
+
+
+ + + Esteban Rogelios + + +
+
+
+
+ + Esteban Rogelios + +
+
+
+ + + + + + + + +
+
+
+ + 93 + +
+
+
+
+ + 93 + +
+
+
+
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/img/tutorial/offset-and-limit/limit2.svg b/docs/img/tutorial/offset-and-limit/limit2.svg deleted file mode 100644 index a3a7e81aa7..0000000000 --- a/docs/img/tutorial/offset-and-limit/limit2.svg +++ /dev/null @@ -1,57 +0,0 @@ -
hero
hero
id
id
name
name
secret_name
secret_name
age
age
1
1
Deadpond
Deadpond
Dive Wilson
Dive Wilson
null
null
2
2
Spider-Boy
Spider-Boy
Pedro Parqueador
Pedro Parqueador
null
null
3
3
Rusty-Man
Rusty-Man
Tommy Sharp
Tommy Sharp
48
48
4
4
Tarantula
Tarantula
Natalia Roman-on
Natalia Roman-on
32
32
5
5
Black Lion
Black Lion
Trevor Challa
Trevor Challa
35
35
6
6
Dr. Weird
Dr. Weird
Steve Weird
Steve Weird
36
36
7
7
Captain North America
Captain North America
Esteban Rogelios
Esteban Rogelios
93
93
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/img/tutorial/offset-and-limit/limit3.drawio b/docs/img/tutorial/offset-and-limit/limit3.drawio deleted file mode 100644 index 2b2b20db2c..0000000000 --- a/docs/img/tutorial/offset-and-limit/limit3.drawio +++ /dev/null @@ -1,133 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/img/tutorial/offset-and-limit/limit3.drawio.svg b/docs/img/tutorial/offset-and-limit/limit3.drawio.svg new file mode 100644 index 0000000000..30b1c9f85a --- /dev/null +++ b/docs/img/tutorial/offset-and-limit/limit3.drawio.svg @@ -0,0 +1,1031 @@ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + hero + +
+
+
+
+ + hero + +
+
+
+ + + + + + + + + + + +
+
+
+ + + id + + +
+
+
+
+ + id + +
+
+
+ + + + + + + + +
+
+
+ + + name + + +
+
+
+
+ + name + +
+
+
+ + + + + + + + +
+
+
+ + + secret_name + + +
+
+
+
+ + secret_name + +
+
+
+ + + + + + + + +
+
+
+ + + age + + +
+
+
+
+ + age + +
+
+
+ + + + + + + + + + + +
+
+
+ + + 1 + + +
+
+
+
+ + 1 + +
+
+
+ + + + + + + + +
+
+
+ + Deadpond + +
+
+
+
+ + Deadpond + +
+
+
+ + + + + + + + +
+
+
+ + Dive Wilson + +
+
+
+
+ + Dive Wilson + +
+
+
+ + + + + + + + +
+
+
+ + null + +
+
+
+
+ + null + +
+
+
+ + + + + + + + + + + +
+
+
+ + 2 + +
+
+
+
+ + 2 + +
+
+
+ + + + + + + + +
+
+
+ + Spider-Boy + +
+
+
+
+ + Spider-Boy + +
+
+
+ + + + + + + + +
+
+
+ + Pedro Parqueador + +
+
+
+
+ + Pedro Parqueador + +
+
+
+ + + + + + + + +
+
+
+ + null + +
+
+
+
+ + null + +
+
+
+ + + + + + + + + + + +
+
+
+ + 3 + +
+
+
+
+ + 3 + +
+
+
+ + + + + + + + +
+
+
+ + Rusty-Man + +
+
+
+
+ + Rusty-Man + +
+
+
+ + + + + + + + +
+
+
+ + Tommy Sharp + +
+
+
+
+ + Tommy Sharp + +
+
+
+ + + + + + + + +
+
+
+ + 48 + +
+
+
+
+ + 48 + +
+
+
+ + + + + + + + + + + +
+
+
+ + + 4 + + +
+
+
+
+ + 4 + +
+
+
+ + + + + + + + +
+
+
+ + Tarantula + +
+
+
+
+ + Tarantula + +
+
+
+ + + + + + + + +
+
+
+ + + Natalia Roman-on + + +
+
+
+
+ + Natalia Roman-on + +
+
+
+ + + + + + + + +
+
+
+ + 32 + +
+
+
+
+ + 32 + +
+
+
+ + + + + + + + + + + +
+
+
+ + 5 + +
+
+
+
+ + 5 + +
+
+
+ + + + + + + + +
+
+
+ + + Black Lion + + +
+
+
+
+ + Black Lion + +
+
+
+ + + + + + + + +
+
+
+ + + Trevor Challa + + +
+
+
+
+ + Trevor Challa + +
+
+
+ + + + + + + + +
+
+
+ + 35 + +
+
+
+
+ + 35 + +
+
+
+ + + + + + + + + + + +
+
+
+ + 6 + +
+
+
+
+ + 6 + +
+
+
+ + + + + + + + +
+
+
+ + + Dr. Weird + + +
+
+
+
+ + Dr. Weird + +
+
+
+ + + + + + + + +
+
+
+ + + Steve Weird + + +
+
+
+
+ + Steve Weird + +
+
+
+ + + + + + + + +
+
+
+ + 36 + +
+
+
+
+ + 36 + +
+
+
+ + + + + + + + + + + +
+
+
+ + 7 + +
+
+
+
+ + 7 + +
+
+
+ + + + + + + + +
+
+
+ + + Captain North America + + +
+
+
+
+ + Captain North America + +
+
+
+ + + + + + + + +
+
+
+ + + Esteban Rogelios + + +
+
+
+
+ + Esteban Rogelios + +
+
+
+ + + + + + + + +
+
+
+ + 93 + +
+
+
+
+ + 93 + +
+
+
+
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/img/tutorial/offset-and-limit/limit3.svg b/docs/img/tutorial/offset-and-limit/limit3.svg deleted file mode 100644 index 5858359964..0000000000 --- a/docs/img/tutorial/offset-and-limit/limit3.svg +++ /dev/null @@ -1,57 +0,0 @@ -
hero
hero
id
id
name
name
secret_name
secret_name
age
age
1
1
Deadpond
Deadpond
Dive Wilson
Dive Wilson
null
null
2
2
Spider-Boy
Spider-Boy
Pedro Parqueador
Pedro Parqueador
null
null
3
3
Rusty-Man
Rusty-Man
Tommy Sharp
Tommy Sharp
48
48
4
4
Tarantula
Tarantula
Natalia Roman-on
Natalia Roman-on
32
32
5
5
Black Lion
Black Lion
Trevor Challa
Trevor Challa
35
35
6
6
Dr. Weird
Dr. Weird
Steve Weird
Steve Weird
36
36
7
7
Captain North America
Captain North America
Esteban Rogelios
Esteban Rogelios
93
93
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/img/tutorial/relationships/attributes/back-populates.drawio b/docs/img/tutorial/relationships/attributes/back-populates.drawio deleted file mode 100644 index 5ef9c03264..0000000000 --- a/docs/img/tutorial/relationships/attributes/back-populates.drawio +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/img/tutorial/relationships/attributes/back-populates.drawio.svg b/docs/img/tutorial/relationships/attributes/back-populates.drawio.svg new file mode 100644 index 0000000000..434eab1c32 --- /dev/null +++ b/docs/img/tutorial/relationships/attributes/back-populates.drawio.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/docs/img/tutorial/relationships/attributes/back-populates.svg b/docs/img/tutorial/relationships/attributes/back-populates.svg deleted file mode 100644 index dabf36de1f..0000000000 --- a/docs/img/tutorial/relationships/attributes/back-populates.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/img/tutorial/relationships/attributes/back-populates2.drawio b/docs/img/tutorial/relationships/attributes/back-populates2.drawio deleted file mode 100644 index 7313c464e9..0000000000 --- a/docs/img/tutorial/relationships/attributes/back-populates2.drawio +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/img/tutorial/relationships/attributes/back-populates2.drawio.svg b/docs/img/tutorial/relationships/attributes/back-populates2.drawio.svg new file mode 100644 index 0000000000..b94723dc19 --- /dev/null +++ b/docs/img/tutorial/relationships/attributes/back-populates2.drawio.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/docs/img/tutorial/relationships/attributes/back-populates2.svg b/docs/img/tutorial/relationships/attributes/back-populates2.svg deleted file mode 100644 index 83ed8154a6..0000000000 --- a/docs/img/tutorial/relationships/attributes/back-populates2.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/img/tutorial/relationships/select/relationships2.drawio b/docs/img/tutorial/relationships/select/relationships2.drawio deleted file mode 100644 index 0086af56c4..0000000000 --- a/docs/img/tutorial/relationships/select/relationships2.drawio +++ /dev/null @@ -1,142 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/img/tutorial/relationships/select/relationships2.drawio.svg b/docs/img/tutorial/relationships/select/relationships2.drawio.svg new file mode 100644 index 0000000000..131c74dc15 --- /dev/null +++ b/docs/img/tutorial/relationships/select/relationships2.drawio.svg @@ -0,0 +1,988 @@ + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + hero + +
+
+
+
+ + hero + +
+
+
+ + + + + + + + + + + +
+
+
+ + + id + + +
+
+
+
+ + id + +
+
+
+ + + + + + + + +
+
+
+ + + name + + +
+
+
+
+ + name + +
+
+
+ + + + + + + + +
+
+
+ + + secret_name + + +
+
+
+
+ + secret_name + +
+
+
+ + + + + + + + +
+
+
+ + + age + + +
+
+
+
+ + age + +
+
+
+ + + + + + + + +
+
+
+ + + team_id + + +
+
+
+
+ + team_id + +
+
+
+ + + + + + + + + + + +
+
+
+ + + 1 + + +
+
+
+
+ + 1 + +
+
+
+ + + + + + + + +
+
+
+ + Deadpond + +
+
+
+
+ + Deadpond + +
+
+
+ + + + + + + + +
+
+
+ + Dive Wilson + +
+
+
+
+ + Dive Wilson + +
+
+
+ + + + + + + + +
+
+
+ + null + +
+
+
+
+ + null + +
+
+
+ + + + + + + + +
+
+
+ + 2 + +
+
+
+
+ + 2 + +
+
+
+ + + + + + + + + + + +
+
+
+ + 2 + +
+
+
+
+ + 2 + +
+
+
+ + + + + + + + +
+
+
+ + Spider-Boy + +
+
+
+
+ + Spider-Boy + +
+
+
+ + + + + + + + +
+
+
+ + Pedro Parqueador + +
+
+
+
+ + Pedro Parqueador + +
+
+
+ + + + + + + + +
+
+
+ + null + +
+
+
+
+ + null + +
+
+
+ + + + + + + + +
+
+
+ + null + +
+
+
+
+ + null + +
+
+
+ + + + + + + + + + + +
+
+
+ + 3 + +
+
+
+
+ + 3 + +
+
+
+ + + + + + + + +
+
+
+ + Rusty-Man + +
+
+
+
+ + Rusty-Man + +
+
+
+ + + + + + + + +
+
+
+ + Tommy Sharp + +
+
+
+
+ + Tommy Sharp + +
+
+
+ + + + + + + + +
+
+
+ + 48 + +
+
+
+
+ + 48 + +
+
+
+ + + + + + + + +
+
+
+ + 1 + +
+
+
+
+ + 1 + +
+
+
+ + + + + + + + + + + + + +
+
+
+ + team + +
+
+
+
+ + team + +
+
+
+ + + + + + + + + + + +
+
+
+ + + id + + +
+
+
+
+ + id + +
+
+
+ + + + + + + + +
+
+
+ + + name + + +
+
+
+
+ + name + +
+
+
+ + + + + + + + +
+
+
+ + + headquarters + + +
+
+
+
+ + headquarters + +
+
+
+ + + + + + + + + + + +
+
+
+ + + 1 + + +
+
+
+
+ + 1 + +
+
+
+ + + + + + + + +
+
+
+ + Preventers + +
+
+
+
+ + Preventers + +
+
+
+ + + + + + + + +
+
+
+ + + Sharp Tower + + +
+
+
+
+ + Sharp Tower + +
+
+
+ + + + + + + + + + + +
+
+
+ + 2 + +
+
+
+
+ + 2 + +
+
+
+ + + + + + + + +
+
+
+ + Z-Force + +
+
+
+
+ + Z-Force + +
+
+
+ + + + + + + + +
+
+
+

+ + Sister Margaret's Bar + +

+
+
+
+
+ + Sister Margaret's Bar + +
+
+
+ + + + + + + + +
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/img/tutorial/relationships/select/relationships2.svg b/docs/img/tutorial/relationships/select/relationships2.svg deleted file mode 100644 index e2f987eba7..0000000000 --- a/docs/img/tutorial/relationships/select/relationships2.svg +++ /dev/null @@ -1,57 +0,0 @@ -
hero
hero
id
id
name
name
secret_name
secret_name
age
age
team_id
team_id
1
1
Deadpond
Deadpond
Dive Wilson
Dive Wilson
null
null
2
2
2
2
Spider-Boy
Spider-Boy
Pedro Parqueador
Pedro Parqueador
null
null
null
null
3
3
Rusty-Man
Rusty-Man
Tommy Sharp
Tommy Sharp
48
48
1
1
team
team
id
id
name
name
headquarters
headquarters
1
1
Preventers
Preventers
Sharp Tower
Sharp Tower
2
2
Z-Force
Z-Force

Sister Margaret’s Bar

Sister Margaret’s Bar
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 5a63c9da44..cd48b91385 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,19 +1,25 @@ + +

- SQLModel + SQLModel + + SQLModel +

SQLModel, SQL databases in Python, designed for simplicity, compatibility, and robustness.

- - Test - - - Publish + + Test - - Coverage + + Publish + + Coverage Package version @@ -23,7 +29,7 @@ **Documentation**: https://sqlmodel.tiangolo.com -**Source Code**: https://github.com/tiangolo/sqlmodel +**Source Code**: https://github.com/fastapi/sqlmodel --- @@ -39,6 +45,21 @@ The key features are: * **Extensible**: You have all the power of SQLAlchemy and Pydantic underneath. * **Short**: Minimize code duplication. A single type annotation does a lot of work. No need to duplicate models in SQLAlchemy and Pydantic. +## Sponsors + + + +{% if sponsors %} +{% for sponsor in sponsors.gold -%} + +{% endfor -%} +{%- for sponsor in sponsors.silver -%} + +{% endfor %} +{% endif %} + + + ## SQL Databases in FastAPI @@ -51,12 +72,14 @@ It combines SQLAlchemy and Pydantic and tries to simplify the code you write as ## Requirements -A recent and currently supported version of Python (right now, Python supports versions 3.6 and above). +A recent and currently supported version of Python. As **SQLModel** is based on **Pydantic** and **SQLAlchemy**, it requires them. They will be automatically installed when you install SQLModel. ## Installation +Make sure you create a virtual environment, activate it, and then install SQLModel, for example with: +

```console @@ -69,7 +92,7 @@ Successfully installed sqlmodel ## Example -For an introduction to databases, SQL, and everything else, see the SQLModel documentation. +For an introduction to databases, SQL, and everything else, see the SQLModel documentation. Here's a quick example. ✨ @@ -95,16 +118,14 @@ And you want it to have this data: Then you could create a **SQLModel** model like this: ```Python -from typing import Optional - from sqlmodel import Field, SQLModel class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) + id: int | None = Field(default=None, primary_key=True) name: str secret_name: str - age: Optional[int] = None + age: int | None = None ``` That class `Hero` is a **SQLModel** model, the equivalent of a SQL table in Python code. @@ -139,17 +160,15 @@ And **inline errors**: You can learn a lot more about **SQLModel** by quickly following the **tutorial**, but if you need a taste right now of how to put all that together and save to the database, you can do this: -```Python hl_lines="18 21 23-27" -from typing import Optional - +```Python hl_lines="16 19 21-25" from sqlmodel import Field, Session, SQLModel, create_engine class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) + id: int | None = Field(default=None, primary_key=True) name: str secret_name: str - age: Optional[int] = None + age: int | None = None hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") @@ -175,17 +194,15 @@ That will save a **SQLite** database with the 3 heroes. Then you could write queries to select from that same database, for example with: -```Python hl_lines="15-18" -from typing import Optional - +```Python hl_lines="13-17" from sqlmodel import Field, Session, SQLModel, create_engine, select class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) + id: int | None = Field(default=None, primary_key=True) name: str secret_name: str - age: Optional[int] = None + age: int | None = None engine = create_engine("sqlite:///database.db") @@ -212,4 +229,4 @@ And at the same time, ✨ it is also a **Pydantic** model ✨. You can use inher ## License -This project is licensed under the terms of the [MIT license](https://github.com/tiangolo/sqlmodel/blob/main/LICENSE). +This project is licensed under the terms of the [MIT license](https://github.com/fastapi/sqlmodel/blob/main/LICENSE). diff --git a/docs/install.md b/docs/install.md new file mode 100644 index 0000000000..129df3512b --- /dev/null +++ b/docs/install.md @@ -0,0 +1,39 @@ +# Install **SQLModel** + +Create a project directory, create a [virtual environment](virtual-environments.md){.internal-link target=_blank}, activate it, and then install **SQLModel**, for example with: + +
+ +```console +$ pip install sqlmodel +---> 100% +Successfully installed sqlmodel pydantic sqlalchemy +``` + +
+ +As **SQLModel** is built on top of SQLAlchemy and Pydantic, when you install `sqlmodel` they will also be automatically installed. + +## Install DB Browser for SQLite + +Remember that [SQLite is a simple database in a single file](databases.md#a-single-file-database){.internal-link target=_blank}? + +For most of the tutorial I'll use SQLite for the examples. + +Python has integrated support for SQLite, it is a single file read and processed from Python. And it doesn't need an [External Database Server](databases.md#a-server-database){.internal-link target=_blank}, so it will be perfect for learning. + +In fact, SQLite is perfectly capable of handling quite big applications. At some point you might want to migrate to a server-based database like PostgreSQL (which is also free). But for now we'll stick to SQLite. + +Through the tutorial I will show you SQL fragments, and Python examples. And I hope (and expect 🧐) you to actually run them, and verify that the database is working as expected and showing you the same data. + +To be able to explore the SQLite file yourself, independent of Python code (and probably at the same time), I recommend you use DB Browser for SQLite. + +It's a great and simple program to interact with SQLite databases (SQLite files) in a nice user interface. + + + +Go ahead and Install DB Browser for SQLite, it's free. + +## Next Steps + +Okay, let's get going! On the next section we'll start the [Tutorial - User Guide](tutorial/index.md). 🚀 diff --git a/docs/js/custom.js b/docs/js/custom.js index 58f321a05e..ef64c612a9 100644 --- a/docs/js/custom.js +++ b/docs/js/custom.js @@ -13,7 +13,7 @@ function setupTermynal() { function createTermynals() { document - .querySelectorAll(`.${termynalActivateClass} .highlight`) + .querySelectorAll(`.${termynalActivateClass} .highlight code`) .forEach(node => { const text = node.textContent; const lines = text.split("\n"); @@ -110,4 +110,6 @@ async function main() { setupTermynal() } -main() +document$.subscribe(() => { + main() +}) diff --git a/docs/js/termynal.js b/docs/js/termynal.js index c21e437501..45bb371c83 100644 --- a/docs/js/termynal.js +++ b/docs/js/termynal.js @@ -72,14 +72,14 @@ class Termynal { * Initialise the widget, get lines, clear container and start animation. */ init() { - /** + /** * Calculates width and height of Termynal container. * If container is empty and lines are dynamically loaded, defaults to browser `auto` or CSS. - */ + */ const containerStyle = getComputedStyle(this.container); - this.container.style.width = containerStyle.width !== '0px' ? + this.container.style.width = containerStyle.width !== '0px' ? containerStyle.width : undefined; - this.container.style.minHeight = containerStyle.height !== '0px' ? + this.container.style.minHeight = containerStyle.height !== '0px' ? containerStyle.height : undefined; this.container.setAttribute('data-termynal', ''); @@ -138,7 +138,7 @@ class Termynal { restart.innerHTML = "restart ↻" return restart } - + generateFinish() { const finish = document.createElement('a') finish.onclick = (e) => { @@ -215,7 +215,7 @@ class Termynal { /** * Converts line data objects into line elements. - * + * * @param {Object[]} lineData - Dynamically loaded lines. * @param {Object} line - Line data object. * @returns {Element[]} - Array of line elements. @@ -231,7 +231,7 @@ class Termynal { /** * Helper function for generating attributes string. - * + * * @param {Object} line - Line data object. * @returns {string} - String of attributes. */ diff --git a/docs/learn/index.md b/docs/learn/index.md new file mode 100644 index 0000000000..bcf8a0b0f5 --- /dev/null +++ b/docs/learn/index.md @@ -0,0 +1,7 @@ +# Learn + +Learn how to use **SQLModel** here. + +This includes an introduction to **databases**, **SQL**, how to interact with databases from **code** and more. + +You could consider this a **book**, a **course**, and the **official** recommended way to learn **SQLModel**. 😎 diff --git a/docs/management-tasks.md b/docs/management-tasks.md new file mode 100644 index 0000000000..1ca3765c03 --- /dev/null +++ b/docs/management-tasks.md @@ -0,0 +1,115 @@ +# Repository Management Tasks + +These are the tasks that can be performed to manage the SQLModel repository by [team members](./management.md#team){.internal-link target=_blank}. + +/// tip + +This section is useful only to a handful of people, team members with permissions to manage the repository. You can probably skip it. 😉 + +/// + +...so, you are a [team member of SQLModel](./management.md#team){.internal-link target=_blank}? Wow, you are so cool! 😎 + +You can help with everything on [Help SQLModel - Get Help](./help.md){.internal-link target=_blank} the same ways as external contributors. But additionally, there are some tasks that only you (as part of the team) can perform. + +Here are the general instructions for the tasks you can perform. + +Thanks a lot for your help. 🙇 + +## Be Nice + +First of all, be nice. 😊 + +You probably are super nice if you were added to the team, but it's worth mentioning it. 🤓 + +### When Things are Difficult + +When things are great, everything is easier, so that doesn't need much instructions. But when things are difficult, here are some guidelines. + +Try to find the good side. In general, if people are not being unfriendly, try to thank their effort and interest, even if you disagree with the main subject (discussion, PR), just thank them for being interested in the project, or for having dedicated some time to try to do something. + +It's difficult to convey emotion in text, use emojis to help. 😅 + +In discussions and PRs, in many cases, people bring their frustration and show it without filter, in many cases exaggerating, complaining, being entitled, etc. That's really not nice, and when it happens, it lowers our priority to solve their problems. But still, try to breath, and be gentle with your answers. + +Try to avoid using bitter sarcasm or potentially passive-aggressive comments. If something is wrong, it's better to be direct (try to be gentle) than sarcastic. + +Try to be as specific and objective as possible, avoid generalizations. + +For conversations that are more difficult, for example to reject a PR, you can ask me (@tiangolo) to handle it directly. + +## Edit PR Titles + +* Edit the PR title to start with an emoji from gitmoji. + * Use the emoji character, not the GitHub code. So, use `🐛` instead of `:bug:`. This is so that it shows up correctly outside of GitHub, for example in the release notes. +* Start the title with a verb. For example `Add`, `Refactor`, `Fix`, etc. This way the title will say the action that the PR does. Like `Add support for teleporting`, instead of `Teleporting wasn't working, so this PR fixes it`. +* Edit the text of the PR title to start in "imperative", like giving an order. So, instead of `Adding support for teleporting` use `Add support for teleporting`. +* Try to make the title descriptive about what it achieves. If it's a feature, try to describe it, for example `Add support for teleporting` instead of `Create TeleportAdapter class`. +* Do not finish the title with a period (`.`). + +Once the PR is merged, a GitHub Action (latest-changes) will use the PR title to update the latest changes automatically. + +So, having a nice PR title will not only look nice in GitHub, but also in the release notes. 📝 + +## Add Labels to PRs + +The same GitHub Action latest-changes uses one label in the PR to decide the section in the release notes to put this PR in. + +Make sure you use a supported label from the latest-changes list of labels: + +* `breaking`: Breaking Changes + * Existing code will break if they update the version without changing their code. This rarely happens, so this label is not frequently used. +* `security`: Security Fixes + * This is for security fixes, like vulnerabilities. It would almost never be used. +* `feature`: Features + * New features, adding support for things that didn't exist before. +* `bug`: Fixes + * Something that was supported didn't work, and this fixes it. There are many PRs that claim to be bug fixes because the user is doing something in an unexpected way that is not supported, but they considered it what should be supported by default. Many of these are actually features or refactors. But in some cases there's an actual bug. +* `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, 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`. +* `internal`: Internal + * Use this for changes that only affect how the repo is managed. For example upgrades to internal dependencies, changes in GitHub Actions or scripts, etc. + +/// tip + +Some tools like Dependabot, will add some labels, like `dependencies`, but have in mind that this label is not used by the `latest-changes` GitHub Action, so it won't be used in the release notes. Please make sure one of the labels above is added. + +/// + +## Review PRs + +If a PR doesn't explain what it does or why, ask for more information. + +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. +* The docs should include a source example file, not write Python directly in Markdown. +* If the source example(s) file can have different syntax for different Python versions, there should be different versions of the file, and they should be shown in tabs in the docs. +* There should be tests testing the source example. +* Before the PR is applied, the new tests should fail. +* After applying the PR, the new tests should pass. +* Coverage should stay at 100%. +* If you see the PR makes sense, or we discussed it and considered it should be accepted, you can add commits on top of the PR to tweak it, to add docs, tests, format, refactor, remove extra files, etc. +* Feel free to comment in the PR to ask for more information, to suggest changes, etc. +* Once you think the PR is ready, move it in the internal GitHub project for me to review 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`, **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 + +When a question in GitHub Discussions has been answered, mark the answer by clicking "Mark as answer". + +Many of the current Discussion Questions were migrated from old issues. Many have the label `answered`, that means they were answered when they were issues, but now in GitHub Discussions, it's not known what is the actual response from the messages. + +You can filter discussions by `Questions` that are `Unanswered`. diff --git a/docs/management.md b/docs/management.md new file mode 100644 index 0000000000..0078e364c3 --- /dev/null +++ b/docs/management.md @@ -0,0 +1,45 @@ +# Repository Management + +Here's a short description of how the SQLModel repository is managed and maintained. + +## Owner + +I, @tiangolo, am the creator and owner of the SQLModel repository. 🤓 + +I normally give the final review to each PR before merging them. I make the final decisions on the project, I'm the BDFL. 😅 + +## Team + +There's a team of people that help manage and maintain the project. 😎 + +They have different levels of permissions and [specific instructions](./management-tasks.md){.internal-link target=_blank}. + +Some of the tasks they can perform include: + +* Adding labels to PRs. +* Editing PR titles. +* Adding commits on top of PRs to tweak them. +* Mark answers in GitHub Discussions questions, etc. +* Merge some specific types of PRs. + +Joining the team is by invitation only, and I could update or remove permissions, instructions, or membership. + +### Team Members + +This is the current list of team members. 😎 + +
+{% for user in members["members"] %} + + +{% endfor %} + +
+ +Additional to them, there's a large community of people helping each other and getting involved in the projects in different ways. + +## External Contributions + +External contributions are very welcome and appreciated, including answering questions, submitting PRs, etc. 🙇‍♂️ + +There are many ways to [help maintain SQLModel](./help.md){.internal-link target=_blank}. diff --git a/docs/overrides/main.html b/docs/overrides/main.html new file mode 100644 index 0000000000..94d9808cc7 --- /dev/null +++ b/docs/overrides/main.html @@ -0,0 +1 @@ +{% extends "base.html" %} diff --git a/docs/release-notes.md b/docs/release-notes.md index df4b54c546..84790f01c6 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,686 @@ ## Latest Changes +### Internal + +* ⬆ Update fastapi requirement from >=0.103.2,<0.126.0 to >=0.103.2,<0.129.0. PR [#1703](https://github.com/fastapi/sqlmodel/pull/1703) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ✅ Update tests, remove conditionals for Pydantic v1. PR [#1702](https://github.com/fastapi/sqlmodel/pull/1702) by [@tiangolo](https://github.com/tiangolo). + +## 0.0.31 + +### Breaking Changes + +* ➖ Drop support for Pydantic v1. PR [#1701](https://github.com/fastapi/sqlmodel/pull/1701) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* ⬆ Bump dirty-equals from 0.9.0 to 0.11. PR [#1649](https://github.com/fastapi/sqlmodel/pull/1649) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump mkdocs-material from 9.7.0 to 9.7.1. PR [#1690](https://github.com/fastapi/sqlmodel/pull/1690) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump typer from 0.20.1 to 0.21.0. PR [#1694](https://github.com/fastapi/sqlmodel/pull/1694) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 📌 Relax `prek` version pin to `>=0.2.24,<1.0.0`. PR [#1698](https://github.com/fastapi/sqlmodel/pull/1698) by [@YuriiMotov](https://github.com/YuriiMotov). + +## 0.0.30 + +### Breaking Changes + +* ➖ Drop support for Python 3.8. PR [#1696](https://github.com/fastapi/sqlmodel/pull/1696) by [@tiangolo](https://github.com/tiangolo). + +### Docs + +* ➖ Drop support for Python 3.8 in CI and docs. PR [#1695](https://github.com/fastapi/sqlmodel/pull/1695) by [@YuriiMotov](https://github.com/YuriiMotov) and [@tiangolo](https://github.com/tiangolo). + +### Internal + +* 🔧 Update pre-commit, generate select on pre-commit, use local Ruff. PR [#1697](https://github.com/fastapi/sqlmodel/pull/1697) by [@tiangolo](https://github.com/tiangolo). +* ⬆ Bump actions/checkout from 5 to 6. PR [#1692](https://github.com/fastapi/sqlmodel/pull/1692) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 👷 Add pre-commit workflow. PR [#1684](https://github.com/fastapi/sqlmodel/pull/1684) by [@YuriiMotov](https://github.com/YuriiMotov). +* ✅ Simplify tests for code examples, one test file for multiple variants. PR [#1664](https://github.com/fastapi/sqlmodel/pull/1664) by [@YuriiMotov](https://github.com/YuriiMotov). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1677](https://github.com/fastapi/sqlmodel/pull/1677) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump actions/download-artifact from 6 to 7. PR [#1676](https://github.com/fastapi/sqlmodel/pull/1676) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump actions/cache from 4 to 5. PR [#1673](https://github.com/fastapi/sqlmodel/pull/1673) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump markdown-include-variants from 0.0.5 to 0.0.8. PR [#1674](https://github.com/fastapi/sqlmodel/pull/1674) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump actions/upload-artifact from 5 to 6. PR [#1675](https://github.com/fastapi/sqlmodel/pull/1675) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump mypy from 1.18.2 to 1.19.1. PR [#1679](https://github.com/fastapi/sqlmodel/pull/1679) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump typer from 0.20.0 to 0.20.1. PR [#1685](https://github.com/fastapi/sqlmodel/pull/1685) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.14.8 to 0.14.10. PR [#1681](https://github.com/fastapi/sqlmodel/pull/1681) by [@dependabot[bot]](https://github.com/apps/dependabot). + +## 0.0.29 + +### Fixes + +* 🐛 Fix `alias` support for Pydantic v2. PR [#1577](https://github.com/fastapi/sqlmodel/pull/1577) by [@ravishan16](https://github.com/ravishan16). + +## 0.0.28 + +### Fixes + +* 🐛 Fix `RuntimeError: dictionary changed size during iteration` in `sqlmodel_update()`. PR [#997](https://github.com/fastapi/sqlmodel/pull/997) by [@BartSchuurmans](https://github.com/BartSchuurmans). + +### Docs + +* 💅 Update CSS to explicitly use emoji font. PR [#1658](https://github.com/fastapi/sqlmodel/pull/1658) by [@tiangolo](https://github.com/tiangolo). +* 📝 Update link to JetBrains Python survey in `features.md`. PR [#1627](https://github.com/fastapi/sqlmodel/pull/1627) by [@sparkiegeek](https://github.com/sparkiegeek). +* 📝 Fix broken links in docs. PR [#1601](https://github.com/fastapi/sqlmodel/pull/1601) by [@YuriiMotov](https://github.com/YuriiMotov). + +### Internal + +* 📌 Pin FastAPI in tests to 0.125.0 while dropping support for Python 3.8. PR [#1689](https://github.com/fastapi/sqlmodel/pull/1689) by [@tiangolo](https://github.com/tiangolo). +* 👷 Configure coverage, error on main tests, don't wait for Smokeshow. PR [#1683](https://github.com/fastapi/sqlmodel/pull/1683) by [@YuriiMotov](https://github.com/YuriiMotov). +* 👷 Run Smokeshow always, even on test failures. PR [#1682](https://github.com/fastapi/sqlmodel/pull/1682) by [@YuriiMotov](https://github.com/YuriiMotov). +* ⬆ Bump ruff from 0.14.6 to 0.14.8. PR [#1667](https://github.com/fastapi/sqlmodel/pull/1667) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1662](https://github.com/fastapi/sqlmodel/pull/1662) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump actions/checkout from 5 to 6. PR [#1656](https://github.com/fastapi/sqlmodel/pull/1656) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.14.5 to 0.14.6. PR [#1652](https://github.com/fastapi/sqlmodel/pull/1652) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1655](https://github.com/fastapi/sqlmodel/pull/1655) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump actions/checkout from 5 to 6. PR [#1651](https://github.com/fastapi/sqlmodel/pull/1651) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 💄 Use font Fira Code to fix display of Rich panels in docs in Windows. PR [#1653](https://github.com/fastapi/sqlmodel/pull/1653) by [@tiangolo](https://github.com/tiangolo). +* 👷 Upgrade `latest-changes` GitHub Action and pin `actions/checkout@v5`. PR [#1654](https://github.com/fastapi/sqlmodel/pull/1654) by [@svlandeg](https://github.com/svlandeg). +* 🔧 Upgrade Material for MkDocs and remove insiders. PR [#1650](https://github.com/fastapi/sqlmodel/pull/1650) by [@tiangolo](https://github.com/tiangolo). +* ⬆ Bump mkdocs-material from 9.6.23 to 9.7.0. PR [#1645](https://github.com/fastapi/sqlmodel/pull/1645) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump mkdocs-macros-plugin from 1.4.1 to 1.5.0. PR [#1647](https://github.com/fastapi/sqlmodel/pull/1647) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.14.4 to 0.14.5. PR [#1646](https://github.com/fastapi/sqlmodel/pull/1646) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1648](https://github.com/fastapi/sqlmodel/pull/1648) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump ruff from 0.14.3 to 0.14.4. PR [#1640](https://github.com/fastapi/sqlmodel/pull/1640) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1642](https://github.com/fastapi/sqlmodel/pull/1642) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump mkdocs-material from 9.6.22 to 9.6.23. PR [#1637](https://github.com/fastapi/sqlmodel/pull/1637) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.14.2 to 0.14.3. PR [#1633](https://github.com/fastapi/sqlmodel/pull/1633) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1636](https://github.com/fastapi/sqlmodel/pull/1636) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump mkdocs-macros-plugin from 1.4.0 to 1.4.1. PR [#1626](https://github.com/fastapi/sqlmodel/pull/1626) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.14.1 to 0.14.2. PR [#1616](https://github.com/fastapi/sqlmodel/pull/1616) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1625](https://github.com/fastapi/sqlmodel/pull/1625) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* 🔧 Add PEP-639 license metadata. PR [#1624](https://github.com/fastapi/sqlmodel/pull/1624) by [@svlandeg](https://github.com/svlandeg). +* ⬆ Bump griffe-typingdoc from 0.2.9 to 0.3.0. PR [#1615](https://github.com/fastapi/sqlmodel/pull/1615) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump actions/upload-artifact from 4 to 5. PR [#1620](https://github.com/fastapi/sqlmodel/pull/1620) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump actions/download-artifact from 5 to 6. PR [#1621](https://github.com/fastapi/sqlmodel/pull/1621) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.14.0 to 0.14.1. PR [#1614](https://github.com/fastapi/sqlmodel/pull/1614) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.13.2 to 0.14.0. PR [#1592](https://github.com/fastapi/sqlmodel/pull/1592) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1605](https://github.com/fastapi/sqlmodel/pull/1605) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump astral-sh/setup-uv from 6 to 7. PR [#1593](https://github.com/fastapi/sqlmodel/pull/1593) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump mkdocs-material from 9.6.21 to 9.6.22. PR [#1608](https://github.com/fastapi/sqlmodel/pull/1608) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 🔧 Configure reminder for `waiting` label in `issue-manager`. PR [#1609](https://github.com/fastapi/sqlmodel/pull/1609) by [@YuriiMotov](https://github.com/YuriiMotov). +* ⬆ Bump typer from 0.19.2 to 0.20.0. PR [#1612](https://github.com/fastapi/sqlmodel/pull/1612) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ✅ Remove unused type ignores since SQLAlchemy 2.0.44. PR [#1613](https://github.com/fastapi/sqlmodel/pull/1613) by [@svlandeg](https://github.com/svlandeg). + +## 0.0.27 + +### Upgrades + +* ⬆️ Add support for Python 3.14. PR [#1578](https://github.com/fastapi/sqlmodel/pull/1578) by [@svlandeg](https://github.com/svlandeg). + +## 0.0.26 + +### Fixes + +* 🐛 Fix attribute handling in `model_dump` for compatibility with the latest Pydantic versions. PR [#1595](https://github.com/fastapi/sqlmodel/pull/1595) by [@spazm](https://github.com/spazm). + +### Docs + +* 📝 Fix typo in `docs/tutorial/fastapi/simple-hero-api.md`. PR [#1583](https://github.com/fastapi/sqlmodel/pull/1583) by [@kofi-kusi](https://github.com/kofi-kusi). + +### Internal + +* ⬆ Bump mypy from 1.4.1 to 1.18.2. PR [#1560](https://github.com/fastapi/sqlmodel/pull/1560) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ✅ Add test that runs select with 3 or 4 arguments. PR [#1590](https://github.com/fastapi/sqlmodel/pull/1590) by [@svlandeg](https://github.com/svlandeg). +* ⬆ Bump mkdocs-macros-plugin from 1.3.9 to 1.4.0. PR [#1581](https://github.com/fastapi/sqlmodel/pull/1581) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump mkdocs-material from 9.6.20 to 9.6.21. PR [#1588](https://github.com/fastapi/sqlmodel/pull/1588) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1584](https://github.com/fastapi/sqlmodel/pull/1584) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump tiangolo/issue-manager from 0.5.1 to 0.6.0. PR [#1589](https://github.com/fastapi/sqlmodel/pull/1589) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 👷 Update docs previews comment, single comment, add failure status. PR [#1586](https://github.com/fastapi/sqlmodel/pull/1586) by [@tiangolo](https://github.com/tiangolo). +* ⬆ Bump markdown-include-variants from 0.0.4 to 0.0.5. PR [#1582](https://github.com/fastapi/sqlmodel/pull/1582) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump typing-extensions from 4.13.2 to 4.15.0 for Python 3.9+. PR [#1580](https://github.com/fastapi/sqlmodel/pull/1580) by [@svlandeg](https://github.com/svlandeg). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1571](https://github.com/fastapi/sqlmodel/pull/1571) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump typer from 0.17.4 to 0.19.2. PR [#1573](https://github.com/fastapi/sqlmodel/pull/1573) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.13.0 to 0.13.2. PR [#1576](https://github.com/fastapi/sqlmodel/pull/1576) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 💚 Fix CI test suite for Windows and MacOS. PR [#1307](https://github.com/fastapi/sqlmodel/pull/1307) by [@svlandeg](https://github.com/svlandeg). + +## 0.0.25 + +### Features + +* ✨ Add overload for `exec` method to support `insert`, `update`, `delete` statements. PR [#1342](https://github.com/fastapi/sqlmodel/pull/1342) by [@seriaati](https://github.com/seriaati). + +### Upgrades + +* ⬆️ Drop support for Python 3.7, require Python 3.8 or above. PR [#1316](https://github.com/fastapi/sqlmodel/pull/1316) by [@svlandeg](https://github.com/svlandeg). + +### Docs + +* ✏️ Fix typos in `docs/tutorial/relationship-attributes/cascade-delete-relationships.md`. PR [#1543](https://github.com/fastapi/sqlmodel/pull/1543) by [@YuriiMotov](https://github.com/YuriiMotov). +* 🍱 Update SVG files, a single file per diagram, sans-serif fonts. PR [#1373](https://github.com/fastapi/sqlmodel/pull/1373) by [@tiangolo](https://github.com/tiangolo). +* 📝 Grammar tweak in `docs/tutorial/insert.md`. PR [#1368](https://github.com/fastapi/sqlmodel/pull/1368) by [@brettcannon](https://github.com/brettcannon). +* 📝 Update `docs/tutorial/fastapi/relationships.md`. PR [#1365](https://github.com/fastapi/sqlmodel/pull/1365) by [@Foxerine](https://github.com/Foxerine). +* ✏️ Tweak the grammar in `docs/learn/index.md`. PR [#1363](https://github.com/fastapi/sqlmodel/pull/1363) by [@brettcannon](https://github.com/brettcannon). +* 📝 Update all docs references to `Optional` to use the new syntax in Python 3.10, e.g. `int | None`. PR [#1351](https://github.com/fastapi/sqlmodel/pull/1351) by [@tiangolo](https://github.com/tiangolo). +* 📝 Update install and usage with FastAPI CLI in FastAPI tutorial. PR [#1350](https://github.com/fastapi/sqlmodel/pull/1350) by [@tiangolo](https://github.com/tiangolo). +* 📝 Update FastAPI tutorial docs to use the new `model.sqlmodel_update()` instead of old `setattr()`. PR [#1117](https://github.com/fastapi/sqlmodel/pull/1117) by [@jpizquierdo](https://github.com/jpizquierdo). +* ✏️ Update `docs/virtual-environments.md`. PR [#1321](https://github.com/fastapi/sqlmodel/pull/1321) by [@sylvainHellin](https://github.com/sylvainHellin). + +### Internal + +* ⬆ Bump griffe-typingdoc from 0.2.8 to 0.2.9. PR [#1553](https://github.com/fastapi/sqlmodel/pull/1553) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump mkdocs-material from 9.6.17 to 9.6.20. PR [#1565](https://github.com/fastapi/sqlmodel/pull/1565) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump actions/setup-python from 5 to 6. PR [#1551](https://github.com/fastapi/sqlmodel/pull/1551) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.12.12 to 0.13.0. PR [#1559](https://github.com/fastapi/sqlmodel/pull/1559) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1564](https://github.com/fastapi/sqlmodel/pull/1564) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump actions/labeler from 5 to 6. PR [#1549](https://github.com/fastapi/sqlmodel/pull/1549) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1556](https://github.com/fastapi/sqlmodel/pull/1556) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump typer from 0.17.3 to 0.17.4. PR [#1554](https://github.com/fastapi/sqlmodel/pull/1554) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1546](https://github.com/fastapi/sqlmodel/pull/1546) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump ruff from 0.12.10 to 0.12.12. PR [#1548](https://github.com/fastapi/sqlmodel/pull/1548) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump typer from 0.16.1 to 0.17.3. PR [#1547](https://github.com/fastapi/sqlmodel/pull/1547) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pypa/gh-action-pypi-publish from 1.12.4 to 1.13.0. PR [#1550](https://github.com/fastapi/sqlmodel/pull/1550) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 👷 Detect and label merge conflicts on PRs automatically. PR [#1552](https://github.com/fastapi/sqlmodel/pull/1552) by [@svlandeg](https://github.com/svlandeg). +* ⬆ Bump ruff from 0.12.9 to 0.12.10. PR [#1532](https://github.com/fastapi/sqlmodel/pull/1532) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1534](https://github.com/fastapi/sqlmodel/pull/1534) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump typer from 0.16.0 to 0.16.1. PR [#1531](https://github.com/fastapi/sqlmodel/pull/1531) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump actions/download-artifact from 4 to 5. PR [#1451](https://github.com/fastapi/sqlmodel/pull/1451) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump actions/checkout from 4 to 5. PR [#1488](https://github.com/fastapi/sqlmodel/pull/1488) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1479](https://github.com/fastapi/sqlmodel/pull/1479) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump mkdocs-macros-plugin from 1.3.7 to 1.3.9. PR [#1507](https://github.com/fastapi/sqlmodel/pull/1507) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.12.7 to 0.12.9. PR [#1521](https://github.com/fastapi/sqlmodel/pull/1521) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump mkdocs-material from 9.6.16 to 9.6.17. PR [#1528](https://github.com/fastapi/sqlmodel/pull/1528) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1444](https://github.com/fastapi/sqlmodel/pull/1444) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump mkdocs-material from 9.6.15 to 9.6.16. PR [#1446](https://github.com/fastapi/sqlmodel/pull/1446) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.12.4 to 0.12.7. PR [#1447](https://github.com/fastapi/sqlmodel/pull/1447) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump tiangolo/latest-changes from 0.3.2 to 0.4.0. PR [#1448](https://github.com/fastapi/sqlmodel/pull/1448) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1437](https://github.com/fastapi/sqlmodel/pull/1437) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump ruff from 0.12.3 to 0.12.4. PR [#1436](https://github.com/fastapi/sqlmodel/pull/1436) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1428](https://github.com/fastapi/sqlmodel/pull/1428) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump ruff from 0.12.2 to 0.12.3. PR [#1432](https://github.com/fastapi/sqlmodel/pull/1432) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1418](https://github.com/fastapi/sqlmodel/pull/1418) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump pillow from 11.2.1 to 11.3.0. PR [#1423](https://github.com/fastapi/sqlmodel/pull/1423) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump mkdocs-material from 9.6.14 to 9.6.15. PR [#1424](https://github.com/fastapi/sqlmodel/pull/1424) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.12.0 to 0.12.2. PR [#1425](https://github.com/fastapi/sqlmodel/pull/1425) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1374](https://github.com/fastapi/sqlmodel/pull/1374) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump ruff from 0.11.13 to 0.12.0. PR [#1403](https://github.com/fastapi/sqlmodel/pull/1403) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ✅ Simplify tests for `tests/test_tutorial/test_code_structure/test_tutorial001.py`, one test file for multiple variants. PR [#1408](https://github.com/fastapi/sqlmodel/pull/1408) by [@tiangolo](https://github.com/tiangolo). +* ✅ Simplify tests setup, one test file for multiple source variants. PR [#1407](https://github.com/fastapi/sqlmodel/pull/1407) by [@tiangolo](https://github.com/tiangolo). +* ✅ Refactor tests to use autouse `clear_sqlmodel`. PR [#1406](https://github.com/fastapi/sqlmodel/pull/1406) by [@tiangolo](https://github.com/tiangolo). +* ⬆ Bump mkdocs-material from 9.5.18 to 9.6.14. PR [#1378](https://github.com/fastapi/sqlmodel/pull/1378) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump typer from 0.15.3 to 0.16.0. PR [#1393](https://github.com/fastapi/sqlmodel/pull/1393) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump cairosvg from 2.7.1 to 2.8.2. PR [#1383](https://github.com/fastapi/sqlmodel/pull/1383) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.11.7 to 0.11.13. PR [#1397](https://github.com/fastapi/sqlmodel/pull/1397) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 🔧 Remove Google Analytics. PR [#1386](https://github.com/fastapi/sqlmodel/pull/1386) by [@tiangolo](https://github.com/tiangolo). +* ⬆ Bump mkdocs-macros-plugin from 1.0.5 to 1.3.7. PR [#1354](https://github.com/fastapi/sqlmodel/pull/1354) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump griffe-typingdoc from 0.2.5 to 0.2.8. PR [#1359](https://github.com/fastapi/sqlmodel/pull/1359) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Update pre-commit requirement from <4.0.0,>=2.17.0 to >=2.17.0,<5.0.0. PR [#1360](https://github.com/fastapi/sqlmodel/pull/1360) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pillow from 11.0.0 to 11.2.1. PR [#1361](https://github.com/fastapi/sqlmodel/pull/1361) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1367](https://github.com/fastapi/sqlmodel/pull/1367) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump ruff from 0.9.6 to 0.11.7. PR [#1355](https://github.com/fastapi/sqlmodel/pull/1355) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1353](https://github.com/fastapi/sqlmodel/pull/1353) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump typing-extensions from 4.12.2 to 4.13.2. PR [#1356](https://github.com/fastapi/sqlmodel/pull/1356) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump typer from 0.15.2 to 0.15.3. PR [#1357](https://github.com/fastapi/sqlmodel/pull/1357) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1339](https://github.com/fastapi/sqlmodel/pull/1339) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump typer from 0.12.3 to 0.15.2. PR [#1325](https://github.com/fastapi/sqlmodel/pull/1325) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump httpx from 0.24.1 to 0.28.1. PR [#1238](https://github.com/fastapi/sqlmodel/pull/1238) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump astral-sh/setup-uv from 5 to 6. PR [#1348](https://github.com/fastapi/sqlmodel/pull/1348) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Update pytest requirement from <8.0.0,>=7.0.1 to >=7.0.1,<9.0.0. PR [#1022](https://github.com/fastapi/sqlmodel/pull/1022) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ♻️ Update `tests/test_select_gen.py`, pass environment variables, needed for NixOS nixpkgs. PR [#969](https://github.com/fastapi/sqlmodel/pull/969) by [@pbsds](https://github.com/pbsds). +* 💚 Fix linting in CI. PR [#1340](https://github.com/fastapi/sqlmodel/pull/1340) by [@svlandeg](https://github.com/svlandeg). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1327](https://github.com/fastapi/sqlmodel/pull/1327) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump jinja2 from 3.1.4 to 3.1.6. PR [#1317](https://github.com/fastapi/sqlmodel/pull/1317) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1319](https://github.com/fastapi/sqlmodel/pull/1319) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). + +## 0.0.24 + +### Upgrades + +* ⬆️ Add support for Python 3.13. PR [#1289](https://github.com/fastapi/sqlmodel/pull/1289) by [@svlandeg](https://github.com/svlandeg). + +### Internal + +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1114](https://github.com/fastapi/sqlmodel/pull/1114) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump ruff from 0.6.2 to 0.9.6. PR [#1294](https://github.com/fastapi/sqlmodel/pull/1294) by [@dependabot[bot]](https://github.com/apps/dependabot). + +## 0.0.23 + +### Fixes + +* 🐛 Fix type annotation in `Field` constructor. PR [#1304](https://github.com/fastapi/sqlmodel/pull/1304) by [@AlanBogarin](https://github.com/AlanBogarin). +* 🐛 Fix Pydantic version check for version 2.10.x onwards. PR [#1255](https://github.com/fastapi/sqlmodel/pull/1255) by [@asiunov](https://github.com/asiunov). + +### Refactors + +* 🚨 Fix types for new Pydantic. PR [#1131](https://github.com/fastapi/sqlmodel/pull/1131) by [@tiangolo](https://github.com/tiangolo). + +### Docs + +* 🩺 Take the GH badge only from pushes to the `main` branch. PR [#1291](https://github.com/fastapi/sqlmodel/pull/1291) by [@svlandeg](https://github.com/svlandeg). +* 📝 Update documentation to refer to `list` instead of `List`. PR [#1147](https://github.com/fastapi/sqlmodel/pull/1147) by [@bubbletroubles](https://github.com/bubbletroubles). +* ✏️ Fix typo in `databases.md`. PR [#1113](https://github.com/fastapi/sqlmodel/pull/1113) by [@radi-dev](https://github.com/radi-dev). +* ✏️ Fix typo in `docs/tutorial/create-db-and-table.md`. PR [#1252](https://github.com/fastapi/sqlmodel/pull/1252) by [@ArianHamdi](https://github.com/ArianHamdi). +* ✏️ Fix typo in `insert.md`. PR [#1256](https://github.com/fastapi/sqlmodel/pull/1256) by [@Noushadaliam](https://github.com/Noushadaliam). +* 📝 Update markdown includes format. PR [#1254](https://github.com/fastapi/sqlmodel/pull/1254) by [@tiangolo](https://github.com/tiangolo). +* 📝 Update fenced code in Decimal docs for consistency. PR [#1251](https://github.com/fastapi/sqlmodel/pull/1251) by [@tiangolo](https://github.com/tiangolo). +* ✏️ Fix typo in the release notes of v0.0.22. PR [#1195](https://github.com/fastapi/sqlmodel/pull/1195) by [@PipeKnight](https://github.com/PipeKnight). +* 📝 Update includes for `docs/advanced/uuid.md`. PR [#1151](https://github.com/fastapi/sqlmodel/pull/1151) by [@tiangolo](https://github.com/tiangolo). +* 📝 Update includes for `docs/tutorial/create-db-and-table.md`. PR [#1149](https://github.com/fastapi/sqlmodel/pull/1149) by [@tiangolo](https://github.com/tiangolo). +* 📝 Fix internal links in docs. PR [#1148](https://github.com/fastapi/sqlmodel/pull/1148) by [@tiangolo](https://github.com/tiangolo). +* ✏️ Fix typo in documentation. PR [#1106](https://github.com/fastapi/sqlmodel/pull/1106) by [@Solipsistmonkey](https://github.com/Solipsistmonkey). +* 📝 Remove highlights in `indexes.md` . PR [#1100](https://github.com/fastapi/sqlmodel/pull/1100) by [@alejsdev](https://github.com/alejsdev). + +### Internal + +* ⬆ Bump pypa/gh-action-pypi-publish from 1.12.3 to 1.12.4. PR [#1277](https://github.com/fastapi/sqlmodel/pull/1277) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 💚 Fix CI test suite for Python 3.7. PR [#1309](https://github.com/fastapi/sqlmodel/pull/1309) by [@svlandeg](https://github.com/svlandeg). +* 👷 Revert "Add Codecov to CI, Smokeshow/Cloudflare has been flaky lately (#1303)". PR [#1306](https://github.com/fastapi/sqlmodel/pull/1306) by [@svlandeg](https://github.com/svlandeg). +* 👷 Add Codecov to CI, Smokeshow/Cloudflare has been flaky lately. PR [#1303](https://github.com/fastapi/sqlmodel/pull/1303) by [@tiangolo](https://github.com/tiangolo). +* 👷 Add retries to Smokeshow. PR [#1302](https://github.com/fastapi/sqlmodel/pull/1302) by [@svlandeg](https://github.com/svlandeg). +* ⬆ Bump astral-sh/setup-uv from 4 to 5. PR [#1249](https://github.com/fastapi/sqlmodel/pull/1249) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pillow from 10.3.0 to 11.0.0. PR [#1139](https://github.com/fastapi/sqlmodel/pull/1139) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pypa/gh-action-pypi-publish from 1.9.0 to 1.12.3. PR [#1240](https://github.com/fastapi/sqlmodel/pull/1240) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump astral-sh/setup-uv from 3 to 4. PR [#1225](https://github.com/fastapi/sqlmodel/pull/1225) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump tiangolo/latest-changes from 0.3.1 to 0.3.2. PR [#1207](https://github.com/fastapi/sqlmodel/pull/1207) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 🔨 Update docs previews script. PR [#1236](https://github.com/fastapi/sqlmodel/pull/1236) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Update build-docs filter paths. PR [#1235](https://github.com/fastapi/sqlmodel/pull/1235) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Update team members. PR [#1234](https://github.com/fastapi/sqlmodel/pull/1234) by [@tiangolo](https://github.com/tiangolo). +* ⬆️ Upgrade markdown-include-variants to version 0.0.3. PR [#1152](https://github.com/fastapi/sqlmodel/pull/1152) by [@tiangolo](https://github.com/tiangolo). +* 👷 Update issue manager workflow. PR [#1137](https://github.com/fastapi/sqlmodel/pull/1137) by [@alejsdev](https://github.com/alejsdev). +* 👷 Fix smokeshow, checkout files on CI. PR [#1136](https://github.com/fastapi/sqlmodel/pull/1136) by [@tiangolo](https://github.com/tiangolo). +* 👷 Use uv in CI. PR [#1135](https://github.com/fastapi/sqlmodel/pull/1135) by [@tiangolo](https://github.com/tiangolo). +* ➕ Add docs dependency markdown-include-variants. PR [#1129](https://github.com/fastapi/sqlmodel/pull/1129) by [@tiangolo](https://github.com/tiangolo). +* 🔨 Update script to standardize format. PR [#1130](https://github.com/fastapi/sqlmodel/pull/1130) by [@tiangolo](https://github.com/tiangolo). +* 👷 Update `labeler.yml`. PR [#1128](https://github.com/fastapi/sqlmodel/pull/1128) by [@tiangolo](https://github.com/tiangolo). +* 👷 Update worfkow deploy-docs-notify URL. PR [#1126](https://github.com/fastapi/sqlmodel/pull/1126) by [@tiangolo](https://github.com/tiangolo). +* 👷 Upgrade Cloudflare GitHub Action. PR [#1124](https://github.com/fastapi/sqlmodel/pull/1124) by [@tiangolo](https://github.com/tiangolo). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1097](https://github.com/fastapi/sqlmodel/pull/1097) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump tiangolo/issue-manager from 0.5.0 to 0.5.1. PR [#1107](https://github.com/fastapi/sqlmodel/pull/1107) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 👷 Update `issue-manager.yml`. PR [#1103](https://github.com/fastapi/sqlmodel/pull/1103) by [@tiangolo](https://github.com/tiangolo). +* 👷 Fix coverage processing in CI, one name per matrix run. PR [#1104](https://github.com/fastapi/sqlmodel/pull/1104) by [@tiangolo](https://github.com/tiangolo). +* 💚 Set `include-hidden-files` to `True` when using the `upload-artifact` GH action. PR [#1098](https://github.com/fastapi/sqlmodel/pull/1098) by [@svlandeg](https://github.com/svlandeg). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1088](https://github.com/fastapi/sqlmodel/pull/1088) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). + +## 0.0.22 + +### Fixes + +* 🐛 Fix support for types with `Optional[Annotated[x, f()]]`, e.g. `id: Optional[pydantic.UUID4]`. PR [#1093](https://github.com/fastapi/sqlmodel/pull/1093) by [@tiangolo](https://github.com/tiangolo). + +### Docs + +* ✏️ Fix a typo in `docs/virtual-environments.md`. PR [#1085](https://github.com/fastapi/sqlmodel/pull/1085) by [@tiangolo](https://github.com/tiangolo). +* 📝 Add docs for virtual environments and environment variables, update contributing. PR [#1082](https://github.com/fastapi/sqlmodel/pull/1082) by [@tiangolo](https://github.com/tiangolo). +* 📝 Add docs about repo management and team. PR [#1059](https://github.com/tiangolo/sqlmodel/pull/1059) by [@tiangolo](https://github.com/tiangolo). +* ✏️ Fix typo in `cascade_delete` docs. PR [#1030](https://github.com/tiangolo/sqlmodel/pull/1030) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* ✅ Refactor test_enums to make them independent of previous imports. PR [#1095](https://github.com/fastapi/sqlmodel/pull/1095) by [@tiangolo](https://github.com/tiangolo). +* 👷 Update `latest-changes` GitHub Action. PR [#1087](https://github.com/fastapi/sqlmodel/pull/1087) by [@tiangolo](https://github.com/tiangolo). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1028](https://github.com/fastapi/sqlmodel/pull/1028) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump ruff from 0.4.7 to 0.6.2. PR [#1081](https://github.com/fastapi/sqlmodel/pull/1081) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 🔧 Update lint script. PR [#1084](https://github.com/fastapi/sqlmodel/pull/1084) by [@tiangolo](https://github.com/tiangolo). +* 👷 Update Python version for coverage. PR [#1083](https://github.com/fastapi/sqlmodel/pull/1083) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Update coverage config files. PR [#1077](https://github.com/fastapi/sqlmodel/pull/1077) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Add URLs to `pyproject.toml`, show up in PyPI. PR [#1074](https://github.com/fastapi/sqlmodel/pull/1074) by [@tiangolo](https://github.com/tiangolo). +* 👷 Do not sync labels as it overrides manually added labels. PR [#1073](https://github.com/fastapi/sqlmodel/pull/1073) by [@tiangolo](https://github.com/tiangolo). +* 👷 Update configs for GitHub Action labeler, to add only one label. PR [#1072](https://github.com/fastapi/sqlmodel/pull/1072) by [@tiangolo](https://github.com/tiangolo). +* 👷 Update labeler GitHub Actions permissions and dependencies. PR [#1071](https://github.com/fastapi/sqlmodel/pull/1071) by [@tiangolo](https://github.com/tiangolo). +* 👷 Add GitHub Action label-checker. PR [#1069](https://github.com/fastapi/sqlmodel/pull/1069) by [@tiangolo](https://github.com/tiangolo). +* 👷 Add GitHub Action labeler. PR [#1068](https://github.com/fastapi/sqlmodel/pull/1068) by [@tiangolo](https://github.com/tiangolo). +* 👷 Update GitHub Action add-to-project. PR [#1067](https://github.com/fastapi/sqlmodel/pull/1067) by [@tiangolo](https://github.com/tiangolo). +* 👷 Add GitHub Action add-to-project. PR [#1066](https://github.com/fastapi/sqlmodel/pull/1066) by [@tiangolo](https://github.com/tiangolo). +* 📝 Update admonitions in annotations. PR [#1065](https://github.com/fastapi/sqlmodel/pull/1065) by [@tiangolo](https://github.com/tiangolo). +* 📝 Update links from github.com/tiangolo/sqlmodel to github.com/fastapi/sqlmodel. PR [#1064](https://github.com/fastapi/sqlmodel/pull/1064) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Update members. PR [#1063](https://github.com/tiangolo/sqlmodel/pull/1063) by [@tiangolo](https://github.com/tiangolo). +* 💄 Add dark-mode logo. PR [#1061](https://github.com/tiangolo/sqlmodel/pull/1061) by [@tiangolo](https://github.com/tiangolo). +* 🔨 Update docs.py script to enable dirty reload conditionally. PR [#1060](https://github.com/tiangolo/sqlmodel/pull/1060) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Update MkDocs previews. PR [#1058](https://github.com/tiangolo/sqlmodel/pull/1058) by [@tiangolo](https://github.com/tiangolo). +* 💄 Update Termynal line-height. PR [#1057](https://github.com/tiangolo/sqlmodel/pull/1057) by [@tiangolo](https://github.com/tiangolo). +* 👷 Upgrade build docs configs. PR [#1047](https://github.com/tiangolo/sqlmodel/pull/1047) by [@tiangolo](https://github.com/tiangolo). +* 👷 Add alls-green for test-redistribute. PR [#1055](https://github.com/tiangolo/sqlmodel/pull/1055) by [@tiangolo](https://github.com/tiangolo). +* 👷 Update docs-previews to handle no docs changes. PR [#1056](https://github.com/tiangolo/sqlmodel/pull/1056) by [@tiangolo](https://github.com/tiangolo). +* 👷🏻 Show docs deployment status and preview URLs in comment. PR [#1054](https://github.com/tiangolo/sqlmodel/pull/1054) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Enable auto dark mode. PR [#1046](https://github.com/tiangolo/sqlmodel/pull/1046) by [@tiangolo](https://github.com/tiangolo). +* 👷 Update issue-manager. PR [#1045](https://github.com/tiangolo/sqlmodel/pull/1045) by [@tiangolo](https://github.com/tiangolo). +* 👷 Update issue-manager.yml GitHub Action permissions. PR [#1040](https://github.com/tiangolo/sqlmodel/pull/1040) by [@tiangolo](https://github.com/tiangolo). +* ♻️ Refactor Deploy Docs GitHub Action to be a script and update token preparing for org. PR [#1039](https://github.com/tiangolo/sqlmodel/pull/1039) by [@tiangolo](https://github.com/tiangolo). + +## 0.0.21 + +### Features + +* ✨ Add support for cascade delete relationships: `cascade_delete`, `ondelete`, and `passive_deletes`. Initial PR [#983](https://github.com/tiangolo/sqlmodel/pull/983) by [@estebanx64](https://github.com/estebanx64). + * New docs at: [Cascade Delete Relationships](https://sqlmodel.tiangolo.com/tutorial/relationship-attributes/cascade-delete-relationships/). + +### Docs + +* 📝 Update docs . PR [#1003](https://github.com/tiangolo/sqlmodel/pull/1003) by [@alejsdev](https://github.com/alejsdev). + +### Internal + +* ⬆ Bump actions/cache from 3 to 4. PR [#783](https://github.com/tiangolo/sqlmodel/pull/783) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump cairosvg from 2.7.0 to 2.7.1. PR [#919](https://github.com/tiangolo/sqlmodel/pull/919) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump jinja2 from 3.1.3 to 3.1.4. PR [#974](https://github.com/tiangolo/sqlmodel/pull/974) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pypa/gh-action-pypi-publish from 1.8.11 to 1.9.0. PR [#987](https://github.com/tiangolo/sqlmodel/pull/987) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump mkdocstrings[python] from 0.23.0 to 0.25.1. PR [#927](https://github.com/tiangolo/sqlmodel/pull/927) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump dorny/paths-filter from 2 to 3. PR [#972](https://github.com/tiangolo/sqlmodel/pull/972) by [@dependabot[bot]](https://github.com/apps/dependabot). + +## 0.0.20 + +### Features + +* ✨ Add official UUID support, docs and tests, internally using new SQLAlchemy 2.0 types. Initial PR [#992](https://github.com/tiangolo/sqlmodel/pull/992) by [@estebanx64](https://github.com/estebanx64). + * New docs in the [Advanced User Guide: UUID (Universally Unique Identifiers)](https://sqlmodel.tiangolo.com/advanced/uuid/). + +### Docs + +* ✏️ Fix internal link in `docs/tutorial/create-db-and-table.md`. PR [#911](https://github.com/tiangolo/sqlmodel/pull/911) by [@tfpgh](https://github.com/tfpgh). +* ✏️ Add missing step in `create-db-and-table-with-db-browser.md`. PR [#976](https://github.com/tiangolo/sqlmodel/pull/976) by [@alejsdev](https://github.com/alejsdev). +* ✏️ Fix typo in `docs/tutorial`. PR [#943](https://github.com/tiangolo/sqlmodel/pull/943) by [@luco17](https://github.com/luco17). +* ✏️ Fix typo in `sqlmodel/_compat.py`. PR [#950](https://github.com/tiangolo/sqlmodel/pull/950) by [@Highfire1](https://github.com/Highfire1). +* ✏️ Update pip installation command in tutorial. PR [#975](https://github.com/tiangolo/sqlmodel/pull/975) by [@alejsdev](https://github.com/alejsdev). +* ✏️ Fix typo in `docs/tutorial/relationship-attributes/index.md`. PR [#880](https://github.com/tiangolo/sqlmodel/pull/880) by [@UncleGoogle](https://github.com/UncleGoogle). + +### Internal + +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#979](https://github.com/tiangolo/sqlmodel/pull/979) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* 🔨 Update docs Termynal scripts to not include line nums for local dev. PR [#1018](https://github.com/tiangolo/sqlmodel/pull/1018) by [@tiangolo](https://github.com/tiangolo). + +## 0.0.19 + +### Fixes + +* 🐛 Fix pydantic `EmailStr` support and `max_length` in several String subclasses. PR [#966](https://github.com/tiangolo/sqlmodel/pull/966) by [@estebanx64](https://github.com/estebanx64). +* 🐛 Fix set varchar limit when `max_length` is set on Pydantic models using Pydantic v2. PR [#963](https://github.com/tiangolo/sqlmodel/pull/963) by [@estebanx64](https://github.com/estebanx64). + +### Refactors + +* ♻️ Refactor generate select template to isolate templated code to the minimum. PR [#967](https://github.com/tiangolo/sqlmodel/pull/967) by [@tiangolo](https://github.com/tiangolo). + +### Upgrades + +* ⬆️ Update minimum SQLAlchemy version to 2.0.14 as that one includes `TryCast` used internally. PR [#964](https://github.com/tiangolo/sqlmodel/pull/964) by [@tiangolo](https://github.com/tiangolo). + +### Docs + +* ✏️ Fix broken link to `@dataclass_transform` (now PEP 681) in `docs/features.md`. PR [#753](https://github.com/tiangolo/sqlmodel/pull/753) by [@soof-golan](https://github.com/soof-golan). + +### Internal + +* ⬆️ Upgrade Ruff and Black. PR [#968](https://github.com/tiangolo/sqlmodel/pull/968) by [@tiangolo](https://github.com/tiangolo). +* ⬆ Bump tiangolo/issue-manager from 0.4.1 to 0.5.0. PR [#922](https://github.com/tiangolo/sqlmodel/pull/922) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 📌 Pin typing-extensions in tests for compatiblity with Python 3.8, dirty-equals, Pydantic. PR [#965](https://github.com/tiangolo/sqlmodel/pull/965) by [@tiangolo](https://github.com/tiangolo). +* 👷 Update GitHub Actions to download and upload artifacts. PR [#936](https://github.com/tiangolo/sqlmodel/pull/936) by [@tiangolo](https://github.com/tiangolo). +* 👷 Tweak CI for test-redistribute, add needed env vars for slim. PR [#929](https://github.com/tiangolo/sqlmodel/pull/929) by [@tiangolo](https://github.com/tiangolo). + +## 0.0.18 + +### Internal + +* ✨ Add `sqlmodel-slim` setup. PR [#916](https://github.com/tiangolo/sqlmodel/pull/916) by [@tiangolo](https://github.com/tiangolo). + +In the future SQLModel will include the standard default recommended packages, and `sqlmodel-slim` will come without those recommended standard packages and with a group of optional dependencies `sqlmodel-slim[standard]`, equivalent to `sqlmodel`, for those that want to opt out of those packages. + +* 🔧 Re-enable MkDocs Material Social plugin. PR [#915](https://github.com/tiangolo/sqlmodel/pull/915) by [@tiangolo](https://github.com/tiangolo). + +## 0.0.17 + +### Refactors + +* ♻️ Refactor types to properly support Pydantic 2.7. PR [#913](https://github.com/tiangolo/sqlmodel/pull/913) by [@tiangolo](https://github.com/tiangolo). + +### Docs + +* 📝 Update ModelRead to ModelPublic documentation and examples. PR [#885](https://github.com/tiangolo/sqlmodel/pull/885) by [@estebanx64](https://github.com/estebanx64). +* ✨ Add source examples for Python 3.10 and 3.9 with updated syntax. PR [#842](https://github.com/tiangolo/sqlmodel/pull/842) by [@tiangolo](https://github.com/tiangolo) and [@estebanx64](https://github.com/estebanx64). + +### Internal + +* ⬆ Bump actions/setup-python from 4 to 5. PR [#733](https://github.com/tiangolo/sqlmodel/pull/733) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 🔨 Update internal scripts and remove unused ones. PR [#914](https://github.com/tiangolo/sqlmodel/pull/914) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Migrate from Poetry to PDM for the internal build config. PR [#912](https://github.com/tiangolo/sqlmodel/pull/912) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Update MkDocs, disable cards while I can upgrade to the latest MkDocs Material, that fixes an issue with social cards. PR [#888](https://github.com/tiangolo/sqlmodel/pull/888) by [@tiangolo](https://github.com/tiangolo). +* 👷 Add cron to run test once a week on monday. PR [#869](https://github.com/tiangolo/sqlmodel/pull/869) by [@estebanx64](https://github.com/estebanx64). +* ⬆️ Upgrade Ruff version and configs. PR [#859](https://github.com/tiangolo/sqlmodel/pull/859) by [@tiangolo](https://github.com/tiangolo). +* 🔥 Remove Jina QA Bot as it has been discontinued. PR [#840](https://github.com/tiangolo/sqlmodel/pull/840) by [@tiangolo](https://github.com/tiangolo). + +## 0.0.16 + +### Features + +* ✨ Add new method `.sqlmodel_update()` to update models in place, including an `update` parameter for extra data. And fix implementation for the (now documented) `update` parameter for `.model_validate()`. PR [#804](https://github.com/tiangolo/sqlmodel/pull/804) by [@tiangolo](https://github.com/tiangolo). + * Updated docs: [Update Data with FastAPI](https://sqlmodel.tiangolo.com/tutorial/fastapi/update/). + * New docs: [Update with Extra Data (Hashed Passwords) with FastAPI](https://sqlmodel.tiangolo.com/tutorial/fastapi/update-extra-data/). + +## 0.0.15 + +### Fixes + +* 🐛 Fix class initialization compatibility with Pydantic and SQLModel, fixing errors revealed by the latest Pydantic. PR [#807](https://github.com/tiangolo/sqlmodel/pull/807) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* ⬆ Bump tiangolo/issue-manager from 0.4.0 to 0.4.1. PR [#775](https://github.com/tiangolo/sqlmodel/pull/775) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 👷 Fix GitHub Actions build docs filter paths for GitHub workflows. PR [#738](https://github.com/tiangolo/sqlmodel/pull/738) by [@tiangolo](https://github.com/tiangolo). + +## 0.0.14 + +### Features + +* ✨ Add support for Pydantic v2 (while keeping support for v1 if v2 is not available). PR [#722](https://github.com/tiangolo/sqlmodel/pull/722) by [@tiangolo](https://github.com/tiangolo) including initial work in PR [#699](https://github.com/tiangolo/sqlmodel/pull/699) by [@AntonDeMeester](https://github.com/AntonDeMeester). + +## 0.0.13 + +### Fixes + +* ♻️ Refactor type generation of selects re-order to prioritize models to optimize editor support. PR [#718](https://github.com/tiangolo/sqlmodel/pull/718) by [@tiangolo](https://github.com/tiangolo). + +### Refactors + +* 🔇 Do not raise deprecation warnings for execute as it's automatically used internally. PR [#716](https://github.com/tiangolo/sqlmodel/pull/716) by [@tiangolo](https://github.com/tiangolo). +* ✅ Move OpenAPI tests inline to simplify updating them with Pydantic v2. PR [#709](https://github.com/tiangolo/sqlmodel/pull/709) by [@tiangolo](https://github.com/tiangolo). + +### Upgrades + +* ⬆️ Add support for Python 3.11 and Python 3.12. PR [#710](https://github.com/tiangolo/sqlmodel/pull/710) by [@tiangolo](https://github.com/tiangolo). + +### Docs + +* ✏️ Fix typo, simplify single quote/apostrophe character in "Sister Margaret's" everywhere in the docs. PR [#721](https://github.com/tiangolo/sqlmodel/pull/721) by [@tiangolo](https://github.com/tiangolo). +* 📝 Update docs for Decimal, use proper types. PR [#719](https://github.com/tiangolo/sqlmodel/pull/719) by [@tiangolo](https://github.com/tiangolo). +* 📝 Add source examples for Python 3.9 and 3.10. PR [#715](https://github.com/tiangolo/sqlmodel/pull/715) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* 🙈 Update gitignore, include all coverage files. PR [#711](https://github.com/tiangolo/sqlmodel/pull/711) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Update config with new pymdown extensions. PR [#712](https://github.com/tiangolo/sqlmodel/pull/712) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Update docs build setup, add support for sponsors, add sponsor GOVCERT.LU. PR [#720](https://github.com/tiangolo/sqlmodel/pull/720) by [@tiangolo](https://github.com/tiangolo). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#697](https://github.com/tiangolo/sqlmodel/pull/697) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* 🔧 Show line numbers in docs during local development. PR [#714](https://github.com/tiangolo/sqlmodel/pull/714) by [@tiangolo](https://github.com/tiangolo). +* 📝 Update details syntax with new pymdown extensions format. PR [#713](https://github.com/tiangolo/sqlmodel/pull/713) by [@tiangolo](https://github.com/tiangolo). + +## 0.0.12 + +### Features + +* ✨ Upgrade SQLAlchemy to 2.0. PR [#700](https://github.com/tiangolo/sqlmodel/pull/700) by [@tiangolo](https://github.com/tiangolo) including initial work in PR [#563](https://github.com/tiangolo/sqlmodel/pull/563) by [@farahats9](https://github.com/farahats9). + +### Internal + +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#686](https://github.com/tiangolo/sqlmodel/pull/686) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* 👷 Upgrade latest-changes GitHub Action. PR [#693](https://github.com/tiangolo/sqlmodel/pull/693) by [@tiangolo](https://github.com/tiangolo). + +## 0.0.11 + +### Features + +* ✨ Add support for passing a custom SQLAlchemy type to `Field()` with `sa_type`. PR [#505](https://github.com/tiangolo/sqlmodel/pull/505) by [@maru0123-2004](https://github.com/maru0123-2004). + * You might consider this a breaking change if you were using an incompatible combination of arguments, those arguments were not taking effect and now you will have a type error and runtime error telling you that. +* ✨ Do not allow invalid combinations of field parameters for columns and relationships, `sa_column` excludes `sa_column_args`, `primary_key`, `nullable`, etc. PR [#681](https://github.com/tiangolo/sqlmodel/pull/681) by [@tiangolo](https://github.com/tiangolo). + +### Docs + +* 🎨 Update inline source examples, hide `#` in annotations (from MkDocs Material). PR [#677](https://github.com/tiangolo/sqlmodel/pull/677) by [@Matthieu-LAURENT39](https://github.com/Matthieu-LAURENT39). + +### Internal + +* ⬆ Update coverage requirement from ^6.2 to >=6.2,<8.0. PR [#663](https://github.com/tiangolo/sqlmodel/pull/663) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Update mkdocs-material requirement from 9.1.21 to 9.2.7. PR [#675](https://github.com/tiangolo/sqlmodel/pull/675) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆️ Upgrade mypy manually. PR [#684](https://github.com/tiangolo/sqlmodel/pull/684) by [@tiangolo](https://github.com/tiangolo). +* ⬆ Update black requirement from ^22.10.0 to >=22.10,<24.0. PR [#664](https://github.com/tiangolo/sqlmodel/pull/664) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 👷 Update CI to build MkDocs Insiders only when the secrets are available, for Dependabot. PR [#683](https://github.com/tiangolo/sqlmodel/pull/683) by [@tiangolo](https://github.com/tiangolo). + +## 0.0.10 + +### Features + +* ✨ Add support for all `Field` parameters from Pydantic `1.9.0` and above, make Pydantic `1.9.0` the minimum required version. PR [#440](https://github.com/tiangolo/sqlmodel/pull/440) by [@daniil-berg](https://github.com/daniil-berg). + +### Internal + +* 🔧 Adopt Ruff for formatting. PR [#679](https://github.com/tiangolo/sqlmodel/pull/679) by [@tiangolo](https://github.com/tiangolo). + +## 0.0.9 + +### Breaking Changes + +* 🗑️ Deprecate Python 3.6 and upgrade Poetry and Poetry Version Plugin. PR [#627](https://github.com/tiangolo/sqlmodel/pull/627) by [@tiangolo](https://github.com/tiangolo). + +### Features + +* ✨ Raise a more clear error when a type is not valid. PR [#425](https://github.com/tiangolo/sqlmodel/pull/425) by [@ddanier](https://github.com/ddanier). + +### Fixes + +* 🐛 Fix `AsyncSession` type annotations for `exec()`. PR [#58](https://github.com/tiangolo/sqlmodel/pull/58) by [@Bobronium](https://github.com/Bobronium). +* 🐛 Fix allowing using a `ForeignKey` directly, remove repeated column construction from `SQLModelMetaclass.__init__` and upgrade minimum SQLAlchemy to `>=1.4.36`. PR [#443](https://github.com/tiangolo/sqlmodel/pull/443) by [@daniil-berg](https://github.com/daniil-berg). +* 🐛 Fix enum type checks ordering in `get_sqlalchemy_type`. PR [#669](https://github.com/tiangolo/sqlmodel/pull/669) by [@tiangolo](https://github.com/tiangolo). +* 🐛 Fix SQLAlchemy version 1.4.36 breaks SQLModel relationships (#315). PR [#461](https://github.com/tiangolo/sqlmodel/pull/461) by [@byrman](https://github.com/byrman). + +### Upgrades + +* ⬆️ Upgrade support for SQLAlchemy 1.4.49, update tests. PR [#519](https://github.com/tiangolo/sqlmodel/pull/519) by [@sandrotosi](https://github.com/sandrotosi). +* ⬆ Raise SQLAlchemy version requirement to at least `1.4.29` (related to #434). PR [#439](https://github.com/tiangolo/sqlmodel/pull/439) by [@daniil-berg](https://github.com/daniil-berg). + +### Docs + +* 📝 Clarify description of in-memory SQLite database in `docs/tutorial/create-db-and-table.md`. PR [#601](https://github.com/tiangolo/sqlmodel/pull/601) by [@SimonCW](https://github.com/SimonCW). +* 📝 Tweak wording in `docs/tutorial/fastapi/multiple-models.md`. PR [#674](https://github.com/tiangolo/sqlmodel/pull/674) by [@tiangolo](https://github.com/tiangolo). +* ✏️ Fix contributing instructions to run tests, update script name. PR [#634](https://github.com/tiangolo/sqlmodel/pull/634) by [@PookieBuns](https://github.com/PookieBuns). +* 📝 Update link to docs for intro to databases. PR [#593](https://github.com/tiangolo/sqlmodel/pull/593) by [@abenezerBelachew](https://github.com/abenezerBelachew). +* 📝 Update docs, use `offset` in example with `limit` and `where`. PR [#273](https://github.com/tiangolo/sqlmodel/pull/273) by [@jbmchuck](https://github.com/jbmchuck). +* 📝 Fix docs for Pydantic's fields using `le` (`lte` is invalid, use `le` ). PR [#207](https://github.com/tiangolo/sqlmodel/pull/207) by [@jrycw](https://github.com/jrycw). +* 📝 Update outdated link in `docs/db-to-code.md`. PR [#649](https://github.com/tiangolo/sqlmodel/pull/649) by [@MatveyF](https://github.com/MatveyF). +* ✏️ Fix typos found with codespell. PR [#520](https://github.com/tiangolo/sqlmodel/pull/520) by [@kianmeng](https://github.com/kianmeng). +* 📝 Fix typos (duplication) in main page. PR [#631](https://github.com/tiangolo/sqlmodel/pull/631) by [@Mr-DRP](https://github.com/Mr-DRP). +* 📝 Update release notes, add second author to PR. PR [#429](https://github.com/tiangolo/sqlmodel/pull/429) by [@br-follow](https://github.com/br-follow). +* 📝 Update instructions about how to make a foreign key required in `docs/tutorial/relationship-attributes/define-relationships-attributes.md`. PR [#474](https://github.com/tiangolo/sqlmodel/pull/474) by [@jalvaradosegura](https://github.com/jalvaradosegura). +* 📝 Update help SQLModel docs. PR [#548](https://github.com/tiangolo/sqlmodel/pull/548) by [@tiangolo](https://github.com/tiangolo). +* ✏️ Fix typo in internal function name `get_sqlachemy_type()`. PR [#496](https://github.com/tiangolo/sqlmodel/pull/496) by [@cmarqu](https://github.com/cmarqu). +* ✏️ Fix typo in docs. PR [#446](https://github.com/tiangolo/sqlmodel/pull/446) by [@davidbrochart](https://github.com/davidbrochart). +* ✏️ Fix typo in `docs/tutorial/create-db-and-table.md`. PR [#477](https://github.com/tiangolo/sqlmodel/pull/477) by [@FluffyDietEngine](https://github.com/FluffyDietEngine). +* ✏️ Fix small typos in docs. PR [#481](https://github.com/tiangolo/sqlmodel/pull/481) by [@micuffaro](https://github.com/micuffaro). + +### Internal + +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#672](https://github.com/tiangolo/sqlmodel/pull/672) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump dawidd6/action-download-artifact from 2.24.2 to 2.28.0. PR [#660](https://github.com/tiangolo/sqlmodel/pull/660) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ✅ Refactor OpenAPI FastAPI tests to simplify updating them later, this moves things around without changes. PR [#671](https://github.com/tiangolo/sqlmodel/pull/671) by [@tiangolo](https://github.com/tiangolo). +* ⬆ Bump actions/checkout from 3 to 4. PR [#670](https://github.com/tiangolo/sqlmodel/pull/670) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 🔧 Update mypy config, use `strict = true` instead of manual configs. PR [#428](https://github.com/tiangolo/sqlmodel/pull/428) by [@michaeloliverx](https://github.com/michaeloliverx). +* ⬆️ Upgrade MkDocs Material. PR [#668](https://github.com/tiangolo/sqlmodel/pull/668) by [@tiangolo](https://github.com/tiangolo). +* 🎨 Update docs format and references with pre-commit and Ruff. PR [#667](https://github.com/tiangolo/sqlmodel/pull/667) by [@tiangolo](https://github.com/tiangolo). +* 🎨 Run pre-commit on all files and autoformat. PR [#666](https://github.com/tiangolo/sqlmodel/pull/666) by [@tiangolo](https://github.com/tiangolo). +* 👷 Move to Ruff and add pre-commit. PR [#661](https://github.com/tiangolo/sqlmodel/pull/661) by [@tiangolo](https://github.com/tiangolo). +* 🛠️ Add `CITATION.cff` file for academic citations. PR [#13](https://github.com/tiangolo/sqlmodel/pull/13) by [@sugatoray](https://github.com/sugatoray). +* 👷 Update docs deployments to Cloudflare. PR [#630](https://github.com/tiangolo/sqlmodel/pull/630) by [@tiangolo](https://github.com/tiangolo). +* 👷‍♂️ Upgrade CI for docs. PR [#628](https://github.com/tiangolo/sqlmodel/pull/628) by [@tiangolo](https://github.com/tiangolo). +* 👷 Update CI debug mode with Tmate. PR [#629](https://github.com/tiangolo/sqlmodel/pull/629) by [@tiangolo](https://github.com/tiangolo). +* 👷 Update latest changes token. PR [#616](https://github.com/tiangolo/sqlmodel/pull/616) by [@tiangolo](https://github.com/tiangolo). +* ⬆️ Upgrade analytics. PR [#558](https://github.com/tiangolo/sqlmodel/pull/558) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Update new issue chooser to point to GitHub Discussions. PR [#546](https://github.com/tiangolo/sqlmodel/pull/546) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Add template for GitHub Discussion questions and update issues template. PR [#544](https://github.com/tiangolo/sqlmodel/pull/544) by [@tiangolo](https://github.com/tiangolo). +* 👷 Refactor CI artifact upload/download for docs previews. PR [#514](https://github.com/tiangolo/sqlmodel/pull/514) by [@tiangolo](https://github.com/tiangolo). +* ⬆ Bump actions/cache from 2 to 3. PR [#497](https://github.com/tiangolo/sqlmodel/pull/497) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump dawidd6/action-download-artifact from 2.24.0 to 2.24.2. PR [#493](https://github.com/tiangolo/sqlmodel/pull/493) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 🔧 Update Smokeshow coverage threshold. PR [#487](https://github.com/tiangolo/sqlmodel/pull/487) by [@tiangolo](https://github.com/tiangolo). +* 👷 Move from Codecov to Smokeshow. PR [#486](https://github.com/tiangolo/sqlmodel/pull/486) by [@tiangolo](https://github.com/tiangolo). +* ⬆ Bump actions/setup-python from 2 to 4. PR [#411](https://github.com/tiangolo/sqlmodel/pull/411) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Update black requirement from ^21.5-beta.1 to ^22.10.0. PR [#460](https://github.com/tiangolo/sqlmodel/pull/460) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ➕ Add extra dev dependencies for MkDocs Material. PR [#485](https://github.com/tiangolo/sqlmodel/pull/485) by [@tiangolo](https://github.com/tiangolo). +* ⬆ Update mypy requirement from 0.930 to 0.971. PR [#380](https://github.com/tiangolo/sqlmodel/pull/380) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Update coverage requirement from ^5.5 to ^6.2. PR [#171](https://github.com/tiangolo/sqlmodel/pull/171) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump codecov/codecov-action from 2 to 3. PR [#415](https://github.com/tiangolo/sqlmodel/pull/415) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump actions/upload-artifact from 2 to 3. PR [#412](https://github.com/tiangolo/sqlmodel/pull/412) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Update flake8 requirement from ^3.9.2 to ^5.0.4. PR [#396](https://github.com/tiangolo/sqlmodel/pull/396) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Update pytest requirement from ^6.2.4 to ^7.0.1. PR [#242](https://github.com/tiangolo/sqlmodel/pull/242) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump actions/checkout from 2 to 3.1.0. PR [#458](https://github.com/tiangolo/sqlmodel/pull/458) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump dawidd6/action-download-artifact from 2.9.0 to 2.24.0. PR [#470](https://github.com/tiangolo/sqlmodel/pull/470) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 👷 Update Dependabot config. PR [#484](https://github.com/tiangolo/sqlmodel/pull/484) by [@tiangolo](https://github.com/tiangolo). + +## 0.0.8 + +### Fixes + +* 🐛 Fix auto detecting and setting `nullable`, allowing overrides in field. PR [#423](https://github.com/tiangolo/sqlmodel/pull/423) by [@JonasKs](https://github.com/JonasKs) and [@br-follow](https://github.com/br-follow). +* ♻️ Update `expresion.py`, sync from Jinja2 template, implement `inherit_cache` to solve errors like: `SAWarning: Class SelectOfScalar will not make use of SQL compilation caching`. PR [#422](https://github.com/tiangolo/sqlmodel/pull/422) by [@tiangolo](https://github.com/tiangolo). + +### Docs + +* 📝 Adjust and clarify docs for `docs/tutorial/create-db-and-table.md`. PR [#426](https://github.com/tiangolo/sqlmodel/pull/426) by [@tiangolo](https://github.com/tiangolo). +* ✏ Fix typo in `docs/tutorial/connect/remove-data-connections.md`. PR [#421](https://github.com/tiangolo/sqlmodel/pull/421) by [@VerdantFox](https://github.com/VerdantFox). + +## 0.0.7 + +### Features + +* ✨ Allow setting `unique` in `Field()` for a column. PR [#83](https://github.com/tiangolo/sqlmodel/pull/83) by [@raphaelgibson](https://github.com/raphaelgibson). +* ✨ Update GUID handling to use stdlib `UUID.hex` instead of an `int`. PR [#26](https://github.com/tiangolo/sqlmodel/pull/26) by [@andrewbolster](https://github.com/andrewbolster). +* ✨ Raise an exception when using a Pydantic field type with no matching SQLAlchemy type. PR [#18](https://github.com/tiangolo/sqlmodel/pull/18) by [@elben10](https://github.com/elben10). +* ⬆ Upgrade constrain for SQLAlchemy = ">=1.4.17,<=1.4.41". PR [#371](https://github.com/tiangolo/sqlmodel/pull/371) by [@RobertRosca](https://github.com/RobertRosca). +* ✨ Add new `Session.get()` parameter `execution_options`. PR [#302](https://github.com/tiangolo/sqlmodel/pull/302) by [@tiangolo](https://github.com/tiangolo). + +### Fixes + +* 🐛 Fix type annotations for `Model.parse_obj()`, and `Model.validate()`. PR [#321](https://github.com/tiangolo/sqlmodel/pull/321) by [@phi-friday](https://github.com/phi-friday). +* 🐛 Fix `Select` and `SelectOfScalar` to inherit cache to avoid warning: `SAWarning: Class SelectOfScalar will not make use of SQL compilation caching`. PR [#234](https://github.com/tiangolo/sqlmodel/pull/234) by [@rabinadk1](https://github.com/rabinadk1). +* 🐛 Fix handling validators for non-default values. PR [#253](https://github.com/tiangolo/sqlmodel/pull/253) by [@byrman](https://github.com/byrman). +* 🐛 Fix fields marked as "set" in models. PR [#117](https://github.com/tiangolo/sqlmodel/pull/117) by [@statt8900](https://github.com/statt8900). +* 🐛 Fix Enum handling in SQLAlchemy. PR [#165](https://github.com/tiangolo/sqlmodel/pull/165) by [@chriswhite199](https://github.com/chriswhite199). +* 🐛 Fix setting nullable property of Fields that don't accept `None`. PR [#79](https://github.com/tiangolo/sqlmodel/pull/79) by [@van51](https://github.com/van51). +* 🐛 Fix SQLAlchemy version 1.4.36 breaks SQLModel relationships (#315). PR [#322](https://github.com/tiangolo/sqlmodel/pull/322) by [@byrman](https://github.com/byrman). + +### Docs + +* 📝 Update docs for models for updating, `id` should not be updatable. PR [#335](https://github.com/tiangolo/sqlmodel/pull/335) by [@kurtportelli](https://github.com/kurtportelli). +* ✏ Fix broken variable/typo in docs for Read Relationships, `hero_spider_boy.id` => `hero_spider_boy.team_id`. PR [#106](https://github.com/tiangolo/sqlmodel/pull/106) by [@yoannmos](https://github.com/yoannmos). +* 🎨 Remove unwanted highlight in the docs. PR [#233](https://github.com/tiangolo/sqlmodel/pull/233) by [@jalvaradosegura](https://github.com/jalvaradosegura). +* ✏ Fix typos in `docs/databases.md` and `docs/tutorial/index.md`. PR [#35](https://github.com/tiangolo/sqlmodel/pull/35) by [@prrao87](https://github.com/prrao87). +* ✏ Fix typo in `docs/tutorial/relationship-attributes/define-relationships-attributes.md`. PR [#239](https://github.com/tiangolo/sqlmodel/pull/239) by [@jalvaradosegura](https://github.com/jalvaradosegura). +* ✏ Fix typo in `docs/tutorial/fastapi/simple-hero-api.md`. PR [#80](https://github.com/tiangolo/sqlmodel/pull/80) by [@joemudryk](https://github.com/joemudryk). +* ✏ Fix typos in multiple files in the docs. PR [#400](https://github.com/tiangolo/sqlmodel/pull/400) by [@VictorGambarini](https://github.com/VictorGambarini). +* ✏ Fix typo in `docs/tutorial/code-structure.md`. PR [#344](https://github.com/tiangolo/sqlmodel/pull/344) by [@marciomazza](https://github.com/marciomazza). +* ✏ Fix typo in `docs/db-to-code.md`. PR [#155](https://github.com/tiangolo/sqlmodel/pull/155) by [@gr8jam](https://github.com/gr8jam). +* ✏ Fix typo in `docs/contributing.md`. PR [#323](https://github.com/tiangolo/sqlmodel/pull/323) by [@Fardad13](https://github.com/Fardad13). +* ✏ Fix typo in `docs/tutorial/fastapi/tests.md`. PR [#265](https://github.com/tiangolo/sqlmodel/pull/265) by [@johnhoman](https://github.com/johnhoman). +* ✏ Fix typo in `docs/tutorial/where.md`. PR [#286](https://github.com/tiangolo/sqlmodel/pull/286) by [@jalvaradosegura](https://github.com/jalvaradosegura). +* ✏ Fix typos in `docs/tutorial/fastapi/update.md`. PR [#268](https://github.com/tiangolo/sqlmodel/pull/268) by [@cirrusj](https://github.com/cirrusj). +* ✏ Fix typo in `docs/tutorial/fastapi/simple-hero-api.md`. PR [#247](https://github.com/tiangolo/sqlmodel/pull/247) by [@hao-wang](https://github.com/hao-wang). +* ✏ Fix typos in `docs/tutorial/automatic-id-none-refresh.md`, `docs/tutorial/fastapi/update.md`, `docs/tutorial/select.md`. PR [#185](https://github.com/tiangolo/sqlmodel/pull/185) by [@rootux](https://github.com/rootux). +* ✏ Fix typo in `docs/databases.md`. PR [#177](https://github.com/tiangolo/sqlmodel/pull/177) by [@seandlg](https://github.com/seandlg). +* ✏ Fix typos in `docs/tutorial/fastapi/update.md`. PR [#162](https://github.com/tiangolo/sqlmodel/pull/162) by [@wmcgee3](https://github.com/wmcgee3). +* ✏ Fix typos in `docs/tutorial/code-structure.md`, `docs/tutorial/fastapi/multiple-models.md`, `docs/tutorial/fastapi/simple-hero-api.md`, `docs/tutorial/many-to-many/index.md`. PR [#116](https://github.com/tiangolo/sqlmodel/pull/116) by [@moonso](https://github.com/moonso). +* ✏ Fix typo in `docs/tutorial/fastapi/teams.md`. PR [#154](https://github.com/tiangolo/sqlmodel/pull/154) by [@chrisgoddard](https://github.com/chrisgoddard). +* ✏ Fix typo variable in example about relationships and `back_populates`, always use `hero` instead of `owner`. PR [#120](https://github.com/tiangolo/sqlmodel/pull/120) by [@onionj](https://github.com/onionj). +* ✏ Fix typo in `docs/tutorial/fastapi/tests.md`. PR [#113](https://github.com/tiangolo/sqlmodel/pull/113) by [@feanil](https://github.com/feanil). +* ✏ Fix typo in `docs/tutorial/where.md`. PR [#72](https://github.com/tiangolo/sqlmodel/pull/72) by [@ZettZet](https://github.com/ZettZet). +* ✏ Fix typo in `docs/tutorial/code-structure.md`. PR [#91](https://github.com/tiangolo/sqlmodel/pull/91) by [@dhiraj](https://github.com/dhiraj). +* ✏ Fix broken link to newsletter sign-up in `docs/help.md`. PR [#84](https://github.com/tiangolo/sqlmodel/pull/84) by [@mborus](https://github.com/mborus). +* ✏ Fix typos in `docs/tutorial/many-to-many/create-models-with-link.md`. PR [#45](https://github.com/tiangolo/sqlmodel/pull/45) by [@xginn8](https://github.com/xginn8). +* ✏ Fix typo in `docs/tutorial/index.md`. PR [#398](https://github.com/tiangolo/sqlmodel/pull/398) by [@ryangrose](https://github.com/ryangrose). + +### Internal + +* ♻ Refactor internal statements to simplify code. PR [#53](https://github.com/tiangolo/sqlmodel/pull/53) by [@yezz123](https://github.com/yezz123). +* ♻ Refactor internal imports to reduce redundancy. PR [#272](https://github.com/tiangolo/sqlmodel/pull/272) by [@aminalaee](https://github.com/aminalaee). +* ⬆ Update development requirement for FastAPI from `^0.68.0` to `^0.68.1`. PR [#48](https://github.com/tiangolo/sqlmodel/pull/48) by [@alucarddelta](https://github.com/alucarddelta). +* ⏪ Revert upgrade Poetry, to make a release that supports Python 3.6 first. PR [#417](https://github.com/tiangolo/sqlmodel/pull/417) by [@tiangolo](https://github.com/tiangolo). +* 👷 Add dependabot for GitHub Actions. PR [#410](https://github.com/tiangolo/sqlmodel/pull/410) by [@tiangolo](https://github.com/tiangolo). +* ⬆️ Upgrade Poetry to version `==1.2.0b1`. PR [#303](https://github.com/tiangolo/sqlmodel/pull/303) by [@tiangolo](https://github.com/tiangolo). +* 👷 Add CI for Python 3.10. PR [#305](https://github.com/tiangolo/sqlmodel/pull/305) by [@tiangolo](https://github.com/tiangolo). +* 📝 Add Jina's QA Bot to the docs to help people that want to ask quick questions. PR [#263](https://github.com/tiangolo/sqlmodel/pull/263) by [@tiangolo](https://github.com/tiangolo). +* 👷 Upgrade Codecov GitHub Action. PR [#304](https://github.com/tiangolo/sqlmodel/pull/304) by [@tiangolo](https://github.com/tiangolo). * 💚 Only run CI on push when on master, to avoid duplicate runs on PRs. PR [#244](https://github.com/tiangolo/sqlmodel/pull/244) by [@tiangolo](https://github.com/tiangolo). * 🔧 Upgrade MkDocs Material and update configs. PR [#217](https://github.com/tiangolo/sqlmodel/pull/217) by [@tiangolo](https://github.com/tiangolo). * ⬆ Upgrade mypy, fix type annotations. PR [#218](https://github.com/tiangolo/sqlmodel/pull/218) by [@tiangolo](https://github.com/tiangolo). diff --git a/docs/resources/index.md b/docs/resources/index.md new file mode 100644 index 0000000000..d233a7833b --- /dev/null +++ b/docs/resources/index.md @@ -0,0 +1,3 @@ +# Resources + +Additional resources, how to **help** and get help, how to **contribute**, and more. ✈️ diff --git a/docs/tutorial/automatic-id-none-refresh.md b/docs/tutorial/automatic-id-none-refresh.md index ed767a2121..93842c6906 100644 --- a/docs/tutorial/automatic-id-none-refresh.md +++ b/docs/tutorial/automatic-id-none-refresh.md @@ -1,27 +1,12 @@ # Automatic IDs, None Defaults, and Refreshing Data -In the previous chapter we saw how to add rows to the database using **SQLModel**. +In the previous chapter, we saw how to add rows to the database using **SQLModel**. Now let's talk a bit about why the `id` field **can't be `NULL`** on the database because it's a **primary key**, and we declare it using `Field(primary_key=True)`. -But the same `id` field actually **can be `None`** in the Python code, so we declare the type with `Optional[int]`, and set the default value to `Field(default=None)`: +But the same `id` field actually **can be `None`** in the Python code, so we declare the type with `int | None`, and set the default value to `Field(default=None)`: -```Python hl_lines="4" -# Code above omitted 👆 - -{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py[ln:6-10]!} - -# Code below ommitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py ln[4:8] hl[5] *} Next, I'll show you a bit more about the synchronization of data between the database and the Python code. @@ -31,32 +16,17 @@ When do we get an actual `int` from the database in that `id` field? Let's see a When we create a new `Hero` instance, we don't set the `id`: -```Python hl_lines="3-6" -# Code above omitted 👆 - -{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py[ln:23-26]!} - -# Code below ommitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py!} -``` +{* ./docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py ln[21:24] hl[21:24] *} -
- -### How `Optional` Helps +### How `int | None` Helps Because we don't set the `id`, it takes the Python's default value of `None` that we set in `Field(default=None)`. -This is the only reason why we define it with `Optional` and with a default value of `None`. +This is the only reason why we define it with `int | None` and with a default value of `None`. Because at this point in the code, **before interacting with the database**, the Python value could actually be `None`. -If we assumed that the `id` was *always* an `int` and added the type annotation without `Optional`, we could end up writing broken code, like: +If we assumed that the `id` was *always* an `int` and added the type annotation without `int | None`, we could end up writing broken code, like: ```Python next_hero_id = hero_1.id + 1 @@ -68,28 +38,13 @@ If we ran this code before saving the hero to the database and the `hero_1.id` w TypeError: unsupported operand type(s) for +: 'NoneType' and 'int' ``` -But by declaring it with `Optional[int]` the editor will help us to avoid writing broken code by showing us a warning telling us that the code could be invalid if `hero_1.id` is `None`. 🔍 +But by declaring it with `int | None`, the editor will help us to avoid writing broken code by showing us a warning telling us that the code could be invalid if `hero_1.id` is `None`. 🔍 ## Print the Default `id` Values We can confirm that by printing our heroes before adding them to the database: -```Python hl_lines="9-11" -# Code above omitted 👆 - -{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py[ln:23-31]!} - -# Code below ommitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py ln[21:29] hl[27:29] *} That will output: @@ -98,7 +53,7 @@ That will output: ```console $ python app.py -// Output above ommitted 👆 +// Output above omitted 👆 Before interacting with the database Hero 1: id=None name='Deadpond' secret_name='Dive Wilson' age=None @@ -118,24 +73,9 @@ What happens when we `add` these objects to the **session**? After we add the `Hero` instance objects to the **session**, the IDs are *still* `None`. -We can verify by creating a session using a `with` block, and adding the objects. And then printing them again: - -```Python hl_lines="19-21" -# Code above omitted 👆 +We can verify by creating a session using a `with` block and adding the objects. And then printing them again: -{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py[ln:23-41]!} - -# Code below ommitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py ln[21:39] hl[37:39] *} This will, again, output the `id`s of the objects as `None`: @@ -144,7 +84,7 @@ This will, again, output the `id`s of the objects as `None`: ```console $ python app.py -// Output above ommitted 👆 +// Output above omitted 👆 After adding to the session Hero 1: id=None name='Deadpond' secret_name='Dive Wilson' age=None @@ -160,22 +100,7 @@ As we saw before, the **session** is smart and doesn't talk to the database ever Then we can `commit` the changes in the session, and print again: -```Python hl_lines="13 16-18" -# Code above omitted 👆 - -{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py[ln:33-48]!} - -# Code below ommitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py ln[31:46] hl[41,44:46] *} And now, something unexpected happens, look at the output, it seems as if the `Hero` instance objects had no data at all: @@ -184,7 +109,7 @@ And now, something unexpected happens, look at the output, it seems as if the `H ```console $ python app.py -// Output above ommitted 👆 +// Output above omitted 👆 // Here the engine talks to the database, the SQL part INFO Engine BEGIN (implicit) @@ -198,9 +123,9 @@ INFO Engine COMMIT // And now our prints After committing the session -Hero 1: -Hero 2: -Hero 3: +Hero 1: +Hero 2: +Hero 3: // What is happening here? 😱 ``` @@ -233,22 +158,7 @@ We didn't access the object's attributes, like `hero.name`. We only accessed the To confirm and understand how this **automatic expiration and refresh** of data when accessing attributes work, we can print some individual fields (instance attributes): -```Python hl_lines="21-23 26-28" -# Code above omitted 👆 - -{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py[ln:33-58]!} - -# Code below ommitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py ln[31:56] hl[49:51,54:56] *} Now we are actually accessing the attributes, because instead of printing the whole object `hero_1`: @@ -271,21 +181,21 @@ Let's see how it works: ```console $ python app.py -// Output above ommitted 👆 +// Output above omitted 👆 // After committing, the objects are expired and have no values After committing the session -Hero 1: -Hero 2: -Hero 3: +Hero 1: +Hero 2: +Hero 3: // Now we will access an attribute like the ID, this is the first print After committing the session, show IDs // Notice that before printing the first ID, the Session makes the Engine go to the database to refresh the data 🤓 INFO Engine BEGIN (implicit) -INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age -FROM hero +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age +FROM hero WHERE hero.id = ? INFO Engine [generated in 0.00017s] (1,) @@ -293,8 +203,8 @@ INFO Engine [generated in 0.00017s] (1,) Hero 1 ID: 1 // Before the next print, refresh the data for the second object -INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age -FROM hero +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age +FROM hero WHERE hero.id = ? INFO Engine [cached since 0.001245s ago] (2,) @@ -302,8 +212,8 @@ INFO Engine [cached since 0.001245s ago] (2,) Hero 2 ID: 2 // Before the third print, refresh its data -INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age -FROM hero +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age +FROM hero WHERE hero.id = ? INFO Engine [cached since 0.002215s ago] (3,) @@ -317,7 +227,6 @@ Hero 2 name: Spider-Boy Hero 3 name: Rusty-Man // Because the Session already refreshed these objects with all their data and the session knows they are not expired, it doesn't have to go again to the database for the names 🤓 - ```
@@ -330,22 +239,7 @@ But what if you want to **explicitly refresh** the data? You can do that too with `session.refresh(object)`: -```Python hl_lines="30-32 35-37" -# Code above omitted 👆 - -{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py[ln:33-67]!} - -# Code below ommitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py ln[31:65] hl[58:60,63:65] *} When Python executes this code: @@ -362,23 +256,23 @@ Here's how the output would look like: ```console $ python app.py -// Output above ommitted 👆 +// Output above omitted 👆 // The first refresh -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.id = ? INFO Engine [generated in 0.00024s] (1,) // The second refresh -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.id = ? INFO Engine [cached since 0.001487s ago] (2,) // The third refresh -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.id = ? INFO Engine [cached since 0.002377s ago] (3,) @@ -403,22 +297,7 @@ Now, as a final experiment, we can also print data after the **session** is clos There are no surprises here, it still works: -```Python hl_lines="40-42" -# Code above omitted 👆 - -{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py[ln:33-72]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py ln[31:70] hl[68:70] *} And the output shows again the same data: @@ -427,7 +306,7 @@ And the output shows again the same data: ```console $ python app.py -// Output above ommitted 👆 +// Output above omitted 👆 // By finishing the with block, the Session is closed, including a rollback of any pending transaction that could have been there and was not committed INFO Engine ROLLBACK @@ -445,17 +324,34 @@ Hero 3: age=48 id=3 name='Rusty-Man' secret_name='Tommy Sharp' Now let's review all this code once again. -!!! tip - Each one of the numbered bubbles shows what each line will print in the output. +/// tip + +Each one of the numbered bubbles shows what each line will print in the output. + +And as we created the **engine** with `echo=True`, we can see the SQL statements being executed at each step. + +/// - And as we created the **engine** with `echo=True`, we can see the SQL statements being executed at each step. +//// tab | Python 3.10+ -```{ .python .annotate hl_lines="54" } -{!./docs_src/tutorial/automatic_id_none_refresh/tutorial002.py!} +```Python +{!./docs_src/tutorial/automatic_id_none_refresh/tutorial002_py310.py!} ``` {!./docs_src/tutorial/automatic_id_none_refresh/annotations/en/tutorial002.md!} +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/automatic_id_none_refresh/tutorial002_py39.py!} +``` + +{!./docs_src/tutorial/automatic_id_none_refresh/annotations/en/tutorial002.md!} + +//// + And here's all the output generated by running this program, all together:
@@ -468,12 +364,12 @@ INFO Engine PRAGMA main.table_info("hero") INFO Engine [raw sql] () INFO Engine PRAGMA temp.table_info("hero") INFO Engine [raw sql] () -INFO Engine +INFO Engine CREATE TABLE hero ( - id INTEGER, - name VARCHAR NOT NULL, - secret_name VARCHAR NOT NULL, - age INTEGER, + id INTEGER, + name VARCHAR NOT NULL, + secret_name VARCHAR NOT NULL, + age INTEGER, PRIMARY KEY (id) ) @@ -497,23 +393,23 @@ INFO Engine INSERT INTO hero (name, secret_name, age) VALUES (?, ?, ?) INFO Engine [cached since 0.001483s ago] ('Rusty-Man', 'Tommy Sharp', 48) INFO Engine COMMIT After committing the session -Hero 1: -Hero 2: -Hero 3: +Hero 1: +Hero 2: +Hero 3: After committing the session, show IDs INFO Engine BEGIN (implicit) -INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age -FROM hero +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age +FROM hero WHERE hero.id = ? INFO Engine [generated in 0.00029s] (1,) Hero 1 ID: 1 -INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age -FROM hero +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age +FROM hero WHERE hero.id = ? INFO Engine [cached since 0.002132s ago] (2,) Hero 2 ID: 2 -INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age -FROM hero +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age +FROM hero WHERE hero.id = ? INFO Engine [cached since 0.003367s ago] (3,) Hero 3 ID: 3 @@ -521,16 +417,16 @@ After committing the session, show names Hero 1 name: Deadpond Hero 2 name: Spider-Boy Hero 3 name: Rusty-Man -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.id = ? INFO Engine [generated in 0.00025s] (1,) -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.id = ? INFO Engine [cached since 0.001583s ago] (2,) -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.id = ? INFO Engine [cached since 0.002722s ago] (3,) After refreshing the heroes diff --git a/docs/tutorial/code-structure.md b/docs/tutorial/code-structure.md index 0d91b4d5f5..08a4165429 100644 --- a/docs/tutorial/code-structure.md +++ b/docs/tutorial/code-structure.md @@ -8,7 +8,7 @@ The class `Hero` has a reference to the class `Team` internally. But the class `Team` also has a reference to the class `Hero`. -So, if those two classes where in separate files and you tried to import the classes in each other's file directly, it would result in a **circular import**. 🔄 +So, if those two classes were in separate files and you tried to import the classes in each other's file directly, it would result in a **circular import**. 🔄 And Python will not be able to handle it and will throw an error. 🚨 @@ -67,9 +67,7 @@ We can use these relative imports because, for example, in the file `app.py` (th You could put all the database Models in a single Python module (a single Python file), for example `models.py`: -```Python -{!./docs_src/tutorial/code_structure/tutorial001/models.py!} -``` +{* ./docs_src/tutorial/code_structure/tutorial001_py310/models.py *} This way, you wouldn't have to deal with circular imports for other models. @@ -79,9 +77,7 @@ And then you could import the models from this file/module in any other file/mod Then you could put the code creating the **engine** and the function to create all the tables (if you are not using migrations) in another file `database.py`: -```Python -{!./docs_src/tutorial/code_structure/tutorial001/database.py!} -``` +{* ./docs_src/tutorial/code_structure/tutorial001_py310/database.py *} This file would also be imported by your application code, to use the shared **engine** and to get and call the function `create_db_and_tables()`. @@ -89,9 +85,7 @@ This file would also be imported by your application code, to use the shared **e Finally, you could put the code to create the **app** in another file `app.py`: -```Python hl_lines="3-4" -{!./docs_src/tutorial/code_structure/tutorial001/app.py!} -``` +{* ./docs_src/tutorial/code_structure/tutorial001_py310/app.py hl[3:4] *} Here we import the models, the engine, and the function to create all the tables and then we can use them all internally. @@ -138,7 +132,7 @@ So, the output would be: $ python -m project.app Created hero: id=1 secret_name='Dive Wilson' team_id=1 name='Deadpond' age=None -Hero's team: name='Z-Force' headquarters='Sister Margaret’s Bar' id=1 +Hero's team: name='Z-Force' headquarters='Sister Margaret's Bar' id=1 ```
@@ -149,10 +143,13 @@ Let's say that for some reason you hate the idea of having all the database mode You can also do it. 😎 There's a couple of things to keep in mind. 🤓 -!!! warning - This is a bit more advanced. +/// warning - If the solution above already worked for you, that might be enough for you, and you can continue in the next chapter. 🤓 +This is a bit more advanced. + +If the solution above already worked for you, that might be enough for you, and you can continue in the next chapter. 🤓 + +/// Let's assume that now the file structure is: @@ -168,9 +165,9 @@ Let's assume that now the file structure is: ### Circular Imports and Type Annotations -The problem with circular imports is that Python can't resolve them at *runtime*. +The problem with circular imports is that Python can't resolve them at *runtime*. -but when using Python **type annotations** it's very common to need to declare the type of some variables with classes imported from other files. +But when using Python **type annotations** it's very common to need to declare the type of some variables with classes imported from other files. And the files with those classes might **also need to import** more things from the first files. @@ -180,7 +177,7 @@ And this ends up *requiring* the same **circular imports** that are not supporte But these **type annotations** we want to declare are not needed at *runtime*. -In fact, remember that we used `List["Hero"]`, with a `"Hero"` in a string? +In fact, remember that we used `list["Hero"]`, with a `"Hero"` in a string? For Python, at runtime, that is **just a string**. @@ -198,15 +195,13 @@ It has a value of `True` for editors and tools that analyze the code with the ty But when Python is executing, its value is `False`. -So, we can us it in an `if` block and import things inside the `if` block. And they will be "imported" only for editors, but not at runtime. +So, we can use it in an `if` block and import things inside the `if` block. And they will be "imported" only for editors, but not at runtime. ### Hero Model File Using that trick of `TYPE_CHECKING` we can "import" the `Team` in `hero_model.py`: -```Python hl_lines="1 5-6 16" -{!./docs_src/tutorial/code_structure/tutorial002/hero_model.py!} -``` +{* ./docs_src/tutorial/code_structure/tutorial002_py310/hero_model.py hl[1,5:6,16] *} Have in mind that now we *have* to put the annotation of `Team` as a string: `"Team"`, so that Python doesn't have errors at runtime. @@ -214,9 +209,7 @@ Have in mind that now we *have* to put the annotation of `Team` as a string: `"T We use the same trick in the `team_model.py` file: -```Python hl_lines="1 5-6 14" -{!./docs_src/tutorial/code_structure/tutorial002/team_model.py!} -``` +{* ./docs_src/tutorial/code_structure/tutorial002_py310/team_model.py hl[1,5:6,14] *} Now we get editor support, autocompletion, inline errors, and **SQLModel** keeps working. 🎉 @@ -224,9 +217,7 @@ Now we get editor support, autocompletion, inline errors, and **SQLModel** keeps Now, just for completeness, the `app.py` file would import the models from both modules: -```Python hl_lines="4-5 10 12-14" -{!./docs_src/tutorial/code_structure/tutorial002/app.py!} -``` +{* ./docs_src/tutorial/code_structure/tutorial002_py310/app.py hl[4:5,10,12:14] *} And of course, all the tricks with `TYPE_CHECKING` and type annotations in strings are **only needed in the files with circular imports**. @@ -240,7 +231,7 @@ And running that achieves the same result as before: $ python -m project.app Created hero: id=1 age=None name='Deadpond' secret_name='Dive Wilson' team_id=1 -Hero's team: id=1 name='Z-Force' headquarters='Sister Margaret’s Bar' +Hero's team: id=1 name='Z-Force' headquarters='Sister Margaret's Bar' ``` diff --git a/docs/tutorial/connect/create-connected-rows.md b/docs/tutorial/connect/create-connected-rows.md index 657edbde43..2f952bf706 100644 --- a/docs/tutorial/connect/create-connected-rows.md +++ b/docs/tutorial/connect/create-connected-rows.md @@ -12,7 +12,7 @@ The `team` table will look like this: - +
1PreventersSharp Tower
2Z-ForceSister Margaret’s Bar2Z-ForceSister Margaret's Bar
@@ -35,21 +35,17 @@ And after we finish working with the data in this chapter, the `hero` table will Each row in the table `hero` will point to a row in the table `team`: -table relationships +table relationships -!!! info - We will later update **Spider-Boy** to add him to the **Preventers** team too, but not yet. +/// info -We will continue with the code in the previous example and we will add more things to it. +We will later update **Spider-Boy** to add him to the **Preventers** team too, but not yet. -
-👀 Full file preview +/// -```Python -{!./docs_src/tutorial/connect/create_tables/tutorial001.py!} -``` +We will continue with the code in the previous example and we will add more things to it. -
+{* ./docs_src/tutorial/connect/create_tables/tutorial001_py310.py ln[0] *} Make sure you remove the `database.db` file before running the examples to get the same results. @@ -61,22 +57,7 @@ And now we will also create the teams there. 🎉 Let's start by creating two teams: -```Python hl_lines="3-9" -# Code above omitted 👆 - -{!./docs_src/tutorial/connect/insert/tutorial001.py[ln:31-37]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/connect/insert/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/connect/insert/tutorial001_py310.py ln[29:35] hl[29:35] *} This would hopefully look already familiar. @@ -92,22 +73,7 @@ And finally we **commit** the session to save the changes to the database. Let's not forget to add this function `create_heroes()` to the `main()` function so that we run it when calling the program from the command line: -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/connect/insert/tutorial001.py[ln:63-65]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/connect/insert/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/connect/insert/tutorial001_py310.py ln[61:63] hl[63] *} ## Run it @@ -126,7 +92,7 @@ INFO Engine BEGIN (implicit) INFO Engine INSERT INTO team (name, headquarters) VALUES (?, ?) INFO Engine [generated in 0.00050s] ('Preventers', 'Sharp Tower') INFO Engine INSERT INTO team (name, headquarters) VALUES (?, ?) -INFO Engine [cached since 0.002324s ago] ('Z-Force', 'Sister Margaret’s Bar') +INFO Engine [cached since 0.002324s ago] ('Z-Force', 'Sister Margaret's Bar') INFO Engine COMMIT ``` @@ -140,26 +106,11 @@ Now let's create one hero object to start. As the `Hero` class model now has a field (column, attribute) `team_id`, we can set it by using the ID field from the `Team` objects we just created before: -```Python hl_lines="12" -# Code above omitted 👆 - -{!./docs_src/tutorial/connect/insert/tutorial001.py[ln:31-41]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/connect/insert/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/connect/insert/tutorial001_py310.py ln[29:39] hl[38] *} We haven't committed this hero to the database yet, but there are already a couple of things to pay **attention** to. -If the database already had some teams, we wouldn't even know **what is the ID** that is going to be automatically assigned to each team by the database, for example, we couldn't just guess `1` or `2`. +If the database already had some teams, we wouldn't even know **what is the ID** that is going to be automatically assigned to each team by the database, for example, we couldn't just guess `1` or `2`. But once the team is created and committed to the database, we can access the object's `id` field to get that ID. @@ -171,41 +122,26 @@ That line alone would generate an output of: ``` INFO Engine BEGIN (implicit) -INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters -FROM team +INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters +FROM team WHERE team.id = ? INFO Engine [generated in 0.00025s] (2,) ``` Let's now create two more heroes: -```Python hl_lines="14-20" -# Code above omitted 👆 - -{!./docs_src/tutorial/connect/insert/tutorial001.py[ln:31-52]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/connect/insert/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/connect/insert/tutorial001_py310.py ln[29:50] hl[40:46] *} When creating `hero_rusty_man`, we are accessing `team_preventers.id`, so that will also trigger a refresh of its data, generating an output of: ``` -INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters -FROM team +INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters +FROM team WHERE team.id = ? INFO Engine [cached since 0.001795s ago] (1,) ``` -There's something else to note. We marked `team_id` as `Optional[int]`, meaning that this could be `NULL` on the database (and `None` in Python). +There's something else to note. We marked `team_id` as `int | None`, meaning that this could be `NULL` on the database (and `None` in Python). That means that a hero doesn't have to have a team. And in this case, **Spider-Boy** doesn't have one. @@ -225,23 +161,7 @@ INFO Engine COMMIT Now let's refresh and print those new heroes to see their new ID pointing to their teams: -```Python hl_lines="26-28 30-32" -# Code above omitted 👆 - -{!./docs_src/tutorial/connect/insert/tutorial001.py[ln:31-60]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/connect/insert/tutorial001.py!} -``` - -
- +{* ./docs_src/tutorial/connect/insert/tutorial001_py310.py ln[29:58] hl[52:54,56:58] *} If we execute that in the command line, it will output: @@ -256,18 +176,18 @@ $ python app.py INFO Engine BEGIN (implicit) // Refresh the first hero -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id +FROM hero WHERE hero.id = ? INFO Engine [generated in 0.00021s] (1,) // Refresh the second hero -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id +FROM hero WHERE hero.id = ? INFO Engine [cached since 0.001575s ago] (2,) // Refresh the third hero -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id +FROM hero WHERE hero.id = ? INFO Engine [cached since 0.002518s ago] (3,) diff --git a/docs/tutorial/connect/create-connected-tables.md b/docs/tutorial/connect/create-connected-tables.md index 452c904ebe..2b1a4f8758 100644 --- a/docs/tutorial/connect/create-connected-tables.md +++ b/docs/tutorial/connect/create-connected-tables.md @@ -16,7 +16,7 @@ The team table will look like this: 1PreventersSharp Tower -2Z-ForceSister Margaret’s Bar +2Z-ForceSister Margaret's Bar @@ -39,7 +39,7 @@ To connect them, we will add another column to the hero table to point to each t This way each row in the table `hero` can point to a row in the table `team`: -table relationships +table relationships ## One-to-Many and Many-to-One @@ -57,20 +57,7 @@ Let's start by creating the tables in code. Import the things we need from `sqlmodel` and create a new `Team` model: -```Python hl_lines="6-9" -{!./docs_src/tutorial/connect/create_tables/tutorial001.py[ln:1-9]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/connect/create_tables/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/connect/create_tables/tutorial001_py310.py ln[1:7] hl[4:7] *} This is very similar to what we have been doing with the `Hero` model. @@ -89,24 +76,11 @@ Now let's create the `hero` table. This is the same model we have been using up to now, we are just adding the new column `team_id`: -```Python hl_lines="18" -{!./docs_src/tutorial/connect/create_tables/tutorial001.py[ln:1-18]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/connect/create_tables/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/connect/create_tables/tutorial001_py310.py ln[1:16] hl[16] *} Most of that should look familiar: -The column will be named `team_id`. It will be an integer, and it could be `NULL` in the database (or `None` in Python), becase there could be some heroes that don't belong to any team. +The column will be named `team_id`. It will be an integer, and it could be `NULL` in the database (or `None` in Python), because there could be some heroes that don't belong to any team. We add a default of `None` to the `Field()` so we don't have to explicitly pass `team_id=None` when creating a hero. @@ -126,49 +100,29 @@ This is the name of the **table** in the database, so it is `"team"`, not the na If you had a custom table name, you would use that custom table name. -!!! info - You can learn about setting a custom table name for a model in the Advanced User Guide. - -### Create the Tables - -Now we can add the same code as before to create the engine and the function to create the tables: +/// info -```Python hl_lines="3-4 6 9-10" -# Code above omitted 👆 +You can learn about setting a custom table name for a model in the Advanced User Guide. -{!./docs_src/tutorial/connect/create_tables/tutorial001.py[ln:21-28]!} -``` +/// -
-👀 Full file preview +### Create the Tables -```Python -{!./docs_src/tutorial/connect/create_tables/tutorial001.py!} -``` +Now we can add the same code as before to create the engine and the function to create the tables: -
+{* ./docs_src/tutorial/connect/create_tables/tutorial001_py310.py ln[19:26] hl[19:20,22,25:26] *} And as before, we'll call this function from another function `main()`, and we'll add that function `main()` to the main block of the file: -```Python hl_lines="3-4 7-8" -# Code above omitted 👆 +{* ./docs_src/tutorial/connect/create_tables/tutorial001_py310.py ln[29:34] hl[29:30,33:34] *} -{!./docs_src/tutorial/connect/create_tables/tutorial001.py[ln:31-36]!} -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/connect/create_tables/tutorial001.py!} -``` +## Run the Code -
+/// tip -## Run the Code +Before running the code, make sure you delete the file `database.db` to make sure you start from scratch. -!!! tip - Before running the code, make sure you delete the file `database.db` to make sure you start from scratch. +/// If we run the code we have up to now, it will go and create the database file `database.db` and the tables in it we just defined, `team` and `hero`: @@ -191,24 +145,24 @@ INFO Engine PRAGMA temp.table_info("hero") INFO Engine [raw sql] () // Create the tables -INFO Engine +INFO Engine CREATE TABLE team ( - id INTEGER, - name VARCHAR NOT NULL, - headquarters VARCHAR NOT NULL, + id INTEGER, + name VARCHAR NOT NULL, + headquarters VARCHAR NOT NULL, PRIMARY KEY (id) ) INFO Engine [no key 0.00010s] () -INFO Engine +INFO Engine CREATE TABLE hero ( - id INTEGER, - name VARCHAR NOT NULL, - secret_name VARCHAR NOT NULL, - age INTEGER, - team_id INTEGER, - PRIMARY KEY (id), + id INTEGER, + name VARCHAR NOT NULL, + secret_name VARCHAR NOT NULL, + age INTEGER, + team_id INTEGER, + PRIMARY KEY (id), FOREIGN KEY(team_id) REFERENCES team (id) ) @@ -229,9 +183,9 @@ So, the first SQL could also be written as: ```SQL CREATE TABLE team ( - id INTEGER, - name TEXT NOT NULL, - headquarters TEXT NOT NULL, + id INTEGER, + name TEXT NOT NULL, + headquarters TEXT NOT NULL, PRIMARY KEY (id) ) ``` @@ -240,12 +194,12 @@ And the second table could be written as: ```SQL hl_lines="8" CREATE TABLE hero ( - id INTEGER, - name TEXT NOT NULL, - secret_name TEXT NOT NULL, - age INTEGER, - team_id INTEGER, - PRIMARY KEY (id), + id INTEGER, + name TEXT NOT NULL, + secret_name TEXT NOT NULL, + age INTEGER, + team_id INTEGER, + PRIMARY KEY (id), FOREIGN KEY(team_id) REFERENCES team (id) ) ``` diff --git a/docs/tutorial/connect/index.md b/docs/tutorial/connect/index.md index 76e0c7bde4..aa57e432fa 100644 --- a/docs/tutorial/connect/index.md +++ b/docs/tutorial/connect/index.md @@ -6,7 +6,10 @@ But the main advantage and feature of SQL databases is being able to handle rela Let's see how to use **SQLModel** to manage connected data in the next chapters. 🤝 -!!! tip - We will extend this further in the next group of chapters making it even more convenient to work with in Python code, using **relationship attributes**. +/// tip - But you should start in this group of chapters first. 🤓 +We will extend this further in the next group of chapters making it even more convenient to work with in Python code, using **relationship attributes**. + +But you should start in this group of chapters first. 🤓 + +/// diff --git a/docs/tutorial/connect/read-connected-data.md b/docs/tutorial/connect/read-connected-data.md index 88cd754607..3fd4607c92 100644 --- a/docs/tutorial/connect/read-connected-data.md +++ b/docs/tutorial/connect/read-connected-data.md @@ -12,7 +12,7 @@ The `team` table has this data: 1PreventersSharp Tower -2Z-ForceSister Margaret’s Bar +2Z-ForceSister Margaret's Bar @@ -35,14 +35,7 @@ And the `hero` table has this data: We will continue with the code in the previous example and we will add more things to it. -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/connect/insert/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/connect/insert/tutorial001_py310.py ln[0] *} ## `SELECT` Connected Data with SQL @@ -62,8 +55,11 @@ FROM hero, team WHERE hero.team_id = team.id ``` -!!! info - Because we have two columns called `name`, one for `hero` and one for `team`, we can specify them with the prefix of the table name and the dot to make it explicit what we refer to. +/// info + +Because we have two columns called `name`, one for `hero` and one for `team`, we can specify them with the prefix of the table name and the dot to make it explicit what we refer to. + +/// Notice that now in the `WHERE` part we are not comparing one column with a literal value (like `hero.name = "Deadpond"`), but we are comparing two columns. @@ -99,14 +95,17 @@ You can go ahead and try it in **DB Browser for SQLite**: -!!! note - Wait, what about Spider-Boy? 😱 +/// note - He doesn't have a team, so his `team_id` is `NULL` in the database. And this SQL is comparing that `NULL` from the `team_id` with all the `id` fields in the rows in the `team` table. +Wait, what about Spider-Boy? 😱 - As there's no team with an ID of `NULL`, it doesn't find a match. +He doesn't have a team, so his `team_id` is `NULL` in the database. And this SQL is comparing that `NULL` from the `team_id` with all the `id` fields in the rows in the `team` table. - But we'll see how to fix that later with a `LEFT JOIN`. +As there's no team with an ID of `NULL`, it doesn't find a match. + +But we'll see how to fix that later with a `LEFT JOIN`. + +/// ## Select Related Data with **SQLModel** @@ -118,22 +117,7 @@ Remember SQLModel's `select()` function? It can take more than one argument. So, we can pass the `Hero` and `Team` model classes. And we can also use both their columns in the `.where()` part: -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/connect/select/tutorial001.py[ln:63-65]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/connect/select/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/connect/select/tutorial001_py310.py ln[61:63] hl[63] *} Notice that in the comparison with `==` we are using the class attributes for both `Hero.team_id` and `Team.id`. @@ -143,53 +127,25 @@ Now we can execute it and get the `results` object. And as we used `select` with two models, we will receive tuples of instances of those two models, so we can iterate over them naturally in a `for` loop: -```Python hl_lines="7" -# Code above omitted 👆 - -{!./docs_src/tutorial/connect/select/tutorial001.py[ln:63-68]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/connect/select/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/connect/select/tutorial001_py310.py ln[61:66] hl[65] *} For each iteration in the `for` loop we get a a tuple with an instance of the class `Hero` and an instance of the class `Team`. And in this `for` loop we assign them to the variable `hero` and the variable `team`. -!!! info - There was a lot of research, design, and work behind **SQLModel** to make this provide the best possible developer experience. - - And you should get autocompletion and inline errors in your editor for both `hero` and `team`. 🎉 - -## Add It to Main - -As always, we must remember to add this new `select_heroes()` function to the `main()` function to make sure it is executed when we call this program from the command line. - -```Python hl_lines="6" -# Code above omitted 👆 +/// info -{!./docs_src/tutorial/connect/select/tutorial001.py[ln:71-74]!} +There was a lot of research, design, and work behind **SQLModel** to make this provide the best possible developer experience. -# Code below omitted 👇 -``` +And you should get autocompletion and inline errors in your editor for both `hero` and `team`. 🎉 -
-👀 Full file preview +/// -```Python -{!./docs_src/tutorial/connect/select/tutorial001.py!} -``` +## Add It to Main -
+As always, we must remember to add this new `select_heroes()` function to the `main()` function to make sure it is executed when we call this program from the command line. +{* ./docs_src/tutorial/connect/select/tutorial001_py310.py ln[69:72] hl[72] *} ## Run the Program @@ -203,13 +159,13 @@ $ python app.py // Previous output omitted 😉 // Get the heroes with their teams -2021-08-09 08:55:50,682 INFO sqlalchemy.engine.Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id, team.id AS id_1, team.name AS name_1, team.headquarters -FROM hero, team +2021-08-09 08:55:50,682 INFO sqlalchemy.engine.Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id, team.id AS id_1, team.name AS name_1, team.headquarters +FROM hero, team WHERE hero.team_id = team.id 2021-08-09 08:55:50,682 INFO sqlalchemy.engine.Engine [no key 0.00015s] () // Print the first hero and team -Hero: id=1 secret_name='Dive Wilson' team_id=2 name='Deadpond' age=None Team: headquarters='Sister Margaret’s Bar' id=2 name='Z-Force' +Hero: id=1 secret_name='Dive Wilson' team_id=2 name='Deadpond' age=None Team: headquarters='Sister Margaret's Bar' id=2 name='Z-Force' // Print the second hero and team Hero: id=2 secret_name='Tommy Sharp' team_id=1 name='Rusty-Man' age=48 Team: headquarters='Sharp Tower' id=1 name='Preventers' @@ -281,10 +237,13 @@ Also in **DB Browser for SQLite**: -!!! tip - Why bother with all this if the result is the same? +/// tip + +Why bother with all this if the result is the same? - This `JOIN` will be useful in a bit to be able to also get Spider-Boy, even if he doesn't have a team. +This `JOIN` will be useful in a bit to be able to also get Spider-Boy, even if he doesn't have a team. + +/// ## Join Tables in **SQLModel** @@ -292,22 +251,7 @@ The same way there's a `.where()` available when using `select()`, there's also And in SQLModel (actually SQLAlchemy), when using the `.join()`, because we already declared what is the `foreign_key` when creating the models, we don't have to pass an `ON` part, it is inferred automatically: -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/connect/select/tutorial002.py[ln:63-68]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/connect/select/tutorial002.py!} -``` - -
+{* ./docs_src/tutorial/connect/select/tutorial002_py310.py ln[61:66] hl[63] *} Also notice that we are still including `Team` in the `select(Hero, Team)`, because we still want to access that data. @@ -323,12 +267,12 @@ $ python app.py // Previous output omitted 😉 // Select using a JOIN with automatic ON -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id, team.id AS id_1, team.name AS name_1, team.headquarters +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id, team.id AS id_1, team.name AS name_1, team.headquarters FROM hero JOIN team ON team.id = hero.team_id INFO Engine [no key 0.00032s] () // Print the first hero and team -Hero: id=1 secret_name='Dive Wilson' team_id=2 name='Deadpond' age=None Team: headquarters='Sister Margaret’s Bar' id=2 name='Z-Force' +Hero: id=1 secret_name='Dive Wilson' team_id=2 name='Deadpond' age=None Team: headquarters='Sister Margaret's Bar' id=2 name='Z-Force' // Print the second hero and team Hero: id=2 secret_name='Tommy Sharp' team_id=1 name='Rusty-Man' age=48 Team: headquarters='Sharp Tower' id=1 name='Preventers' @@ -349,7 +293,7 @@ And then you tell the database `ON` which condition it should join those two tab But by default, only the rows from both left and right that match the condition will be returned. -table relationships +table relationships In this example of tables above 👆, it would return all the heroes, because every hero has a `team_id`, so every hero can be joined with the `team` table: @@ -374,7 +318,7 @@ But in the database that we are working with in the code above, **Spider-Boy** d So there's no way to join the **Spider-Boy** row with some row in the `team` table: -table relationships +table relationships Running the same SQL we used above, the resulting table would not include **Spider-Boy** 😱: @@ -420,8 +364,11 @@ And that would return the following result, including **Spider-Boy** 🎉: -!!! tip - The only difference between this query and the previous is that extra `LEFT OUTER`. +/// tip + +The only difference between this query and the previous is that extra `LEFT OUTER`. + +/// And here's another of the SQL variations, you could write `LEFT OUTER JOIN` or just `LEFT JOIN`, it means the same. @@ -431,22 +378,7 @@ Now let's replicate the same query in **SQLModel**. `.join()` has a parameter we can use `isouter=True` to make the `JOIN` be a `LEFT OUTER JOIN`: -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/connect/select/tutorial003.py[ln:63-68]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/connect/select/tutorial003.py!} -``` - -
+{* ./docs_src/tutorial/connect/select/tutorial003_py310.py ln[61:66] hl[63] *} And if we run it, it will output: @@ -458,13 +390,13 @@ $ python app.py // Previous output omitted 😉 // SELECT using LEFT OUTER JOIN -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id, team.id AS id_1, team.name AS name_1, team.headquarters +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id, team.id AS id_1, team.name AS name_1, team.headquarters FROM hero LEFT OUTER JOIN team ON team.id = hero.team_id INFO Engine [no key 0.00051s] () // Print the first hero and team -Hero: id=1 secret_name='Dive Wilson' team_id=2 name='Deadpond' age=None Team: headquarters='Sister Margaret’s Bar' id=2 name='Z-Force' +Hero: id=1 secret_name='Dive Wilson' team_id=2 name='Deadpond' age=None Team: headquarters='Sister Margaret's Bar' id=2 name='Z-Force' // Print the second hero and team Hero: id=2 secret_name='Tommy Sharp' team_id=1 name='Rusty-Man' age=48 Team: headquarters='Sharp Tower' id=1 name='Preventers' // Print the third hero and team, we included Spider-Boy 🎉 @@ -493,22 +425,7 @@ But we would still be able to **filter** the rows with it. 🤓 We could even add some additional `.where()` after `.join()` to filter the data more, for example to return only the heroes from one team: -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/connect/select/tutorial004.py[ln:63-68]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/connect/select/tutorial004.py!} -``` - -
+{* ./docs_src/tutorial/connect/select/tutorial004_py310.py ln[61:66] hl[63] *} Here we are **filtering** with `.where()` to get only the heroes that belong to the **Preventers** team. @@ -522,9 +439,9 @@ If we run that, it would output: $ python app.py // Select only the hero data -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id // But still join with the team table -FROM hero JOIN team ON team.id = hero.team_id +FROM hero JOIN team ON team.id = hero.team_id // And filter with WHERE to get only the Preventers WHERE team.name = ? INFO Engine [no key 0.00066s] ('Preventers',) @@ -539,22 +456,7 @@ Preventer Hero: id=2 secret_name='Tommy Sharp' team_id=1 name='Rusty-Man' age=48 By putting the `Team` in `select()` we tell **SQLModel** and the database that we want the team data too. -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/connect/select/tutorial005.py[ln:63-68]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/connect/select/tutorial005.py!} -``` - -
+{* ./docs_src/tutorial/connect/select/tutorial005_py310.py ln[61:66] hl[63] *} And if we run that, it will output: @@ -564,9 +466,9 @@ And if we run that, it will output: $ python app.py // Select the hero and the team data -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id, team.id AS id_1, team.name AS name_1, team.headquarters +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id, team.id AS id_1, team.name AS name_1, team.headquarters // Join the hero with the team table -FROM hero JOIN team ON team.id = hero.team_id +FROM hero JOIN team ON team.id = hero.team_id // Filter with WHERE to get only Preventers WHERE team.name = ? INFO Engine [no key 0.00018s] ('Preventers',) diff --git a/docs/tutorial/connect/remove-data-connections.md b/docs/tutorial/connect/remove-data-connections.md index 1153b51f32..d0a0267b1f 100644 --- a/docs/tutorial/connect/remove-data-connections.md +++ b/docs/tutorial/connect/remove-data-connections.md @@ -10,7 +10,7 @@ We currently have a `team` table: 1PreventersSharp Tower -2Z-ForceSister Margaret’s Bar +2Z-ForceSister Margaret's Bar @@ -35,43 +35,17 @@ Let's see how to **remove** connections between rows in tables. We will continue with the code from the previous chapter. -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/connect/update/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/connect/update/tutorial001_py310.py ln[0] *} ## Break a Connection -We don't really have to delete anyting to break a connection. We can just assign `None` to the foreign key, in this case, to the `team_id`. +We don't really have to delete anything to break a connection. We can just assign `None` to the foreign key, in this case, to the `team_id`. Let's say **Spider-Boy** is tired of the lack of friendly neighbors and wants to get out of the **Preventers**. We can simply set the `team_id` to `None`, and now it doesn't have a connection with the team: -```Python hl_lines="8" -# Code above omitted 👆 - -{!./docs_src/tutorial/connect/delete/tutorial001.py[ln:31-32]!} - - # Previous code here omitted 👈 - -{!./docs_src/tutorial/connect/delete/tutorial001.py[ln:68-72]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/connect/delete/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/connect/delete/tutorial001_py310.py ln[29:30,66:70] hl[66] *} Again, we just **assign** a value to that field attribute `team_id`, now the value is `None`, which means `NULL` in the database. Then we `add()` the hero to the session, and then `commit()`. @@ -94,8 +68,8 @@ INFO Engine COMMIT // Automatically start a new transaction INFO Engine BEGIN (implicit) // Refresh the hero -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id +FROM hero WHERE hero.id = ? INFO Engine [cached since 0.1661s ago] (3,) diff --git a/docs/tutorial/connect/update-data-connections.md b/docs/tutorial/connect/update-data-connections.md index b7c8b0daa1..147c4e599d 100644 --- a/docs/tutorial/connect/update-data-connections.md +++ b/docs/tutorial/connect/update-data-connections.md @@ -10,7 +10,7 @@ At this point we have a `team` table: 1PreventersSharp Tower -2Z-ForceSister Margaret’s Bar +2Z-ForceSister Margaret's Bar @@ -37,14 +37,7 @@ Now we'll see how to **update** those connections between rows tables. We will continue with the code we used to create some heroes, and we'll update them. -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/connect/insert/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/connect/insert/tutorial001_py310.py ln[0] *} ## Assign a Team to a Hero @@ -52,26 +45,7 @@ Let's say that **Tommy Sharp** uses his "rich uncle" charms to recruit **Spider- Doing it is just like updating any other field: -```Python hl_lines="8" -# Code above omitted 👆 - -{!./docs_src/tutorial/connect/update/tutorial001.py[ln:31-32]!} - - # Previous code here omitted 👈 - -{!./docs_src/tutorial/connect/update/tutorial001.py[ln:62-66]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/connect/update/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/connect/update/tutorial001_py310.py ln[29:30,60:64] hl[60] *} We can simply **assign** a value to that field attribute `team_id`, then `add()` the hero to the session, and then `commit()`. @@ -94,8 +68,8 @@ INFO Engine COMMIT // Automatically start a new transaction INFO Engine BEGIN (implicit) // Refresh the hero data -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id +FROM hero WHERE hero.id = ? INFO Engine [cached since 0.08837s ago] (3,) diff --git a/docs/tutorial/create-db-and-table-with-db-browser.md b/docs/tutorial/create-db-and-table-with-db-browser.md index a1bb394024..4c535df11c 100644 --- a/docs/tutorial/create-db-and-table-with-db-browser.md +++ b/docs/tutorial/create-db-and-table-with-db-browser.md @@ -40,10 +40,13 @@ Click the button New Database. -A dialog should show up. Go to the [project directory you created](./index.md#create-a-project){.internal-link target=_blank} and save the file with a name of `database.db`. +A dialog should show up. Go to the [project directory you created](../virtual-environments.md#create-a-project){.internal-link target=_blank} and save the file with a name of `database.db`. -!!! tip - It's common to save SQLite database files with an extension of `.db`. Sometimes also `.sqlite`. +/// tip + +It's common to save SQLite database files with an extension of `.db`. Sometimes also `.sqlite`. + +/// ## Create a Table @@ -122,6 +125,8 @@ And delete that `./database.db` file in your project directory. And click again on New Database. +Save the file with the name `database.db` again. + This time, if you see the dialog to create a new table, just close it by clicking the Cancel button. And now, go to the tab Execute SQL. @@ -164,6 +169,6 @@ Of course, you can also go and take a full SQL course or read a book about SQL, We saw how to interact with SQLite databases in files using **DB Browser for SQLite** in a visual user interface. -We also saw how to use it to write some SQL directly to the SQLite database. This will be useful to verify the data in the database is looking correclty, to debug, etc. +We also saw how to use it to write some SQL directly to the SQLite database. This will be useful to verify the data in the database is looking correctly, to debug, etc. In the next chapters we will start using **SQLModel** to interact with the database, and we will continue to use **DB Browser for SQLite** at the same time to look at the database underneath. 🔍 diff --git a/docs/tutorial/create-db-and-table.md b/docs/tutorial/create-db-and-table.md index 2bdecaac67..42ec604932 100644 --- a/docs/tutorial/create-db-and-table.md +++ b/docs/tutorial/create-db-and-table.md @@ -2,7 +2,7 @@ Now let's get to the code. 👩‍💻 -Make sure you are inside of your project directory and with your virtual environment activated as [explained in the previous chapter](index.md){.internal-link target=_blank}. +Make sure you are inside of your project directory and with your virtual environment activated as explained in [Virtual Environments](../virtual-environments.md#create-a-project){.internal-link target=_blank}. We will: @@ -33,34 +33,27 @@ The first thing we need to do is create a class to represent the data in the tab A class like this that represents some data is commonly called a **model**. -!!! tip - That's why this package is called `SQLModel`. Because it's mainly used to create **SQL Models**. +/// tip -For that, we will import `SQLModel` (plus other things we will also use) and create a class `Hero` that inherits from `SQLModel` and represents the **table model** for our heroes: - -```Python hl_lines="3 6" -{!./docs_src/tutorial/create_db_and_table/tutorial001.py[ln:1-10]!} - -# More code here later 👇 -``` +That's why this package is called `SQLModel`. Because it's mainly used to create **SQL Models**. -
-👀 Full file preview +/// -```Python -{!./docs_src/tutorial/create_db_and_table/tutorial001.py!} -``` +For that, we will import `SQLModel` (plus other things we will also use) and create a class `Hero` that inherits from `SQLModel` and represents the **table model** for our heroes: -
+{* ./docs_src/tutorial/create_db_and_table/tutorial001_py310.py ln[1:8] hl[1,4] *} This class `Hero` **represents the table** for our heroes. And each instance we create later will **represent a row** in the table. We use the config `table=True` to tell **SQLModel** that this is a **table model**, it represents a table. -!!! info - It's also possible to have models without `table=True`, those would be only **data models**, without a table in the database, they would not be **table models**. +/// info + +It's also possible to have models without `table=True`, those would be only **data models**, without a table in the database, they would not be **table models**. + +Those **data models** will be **very useful later**, but for now, we'll just keep adding the `table=True` configuration. - Those **data models** will be **very useful later**, but for now, we'll just keep adding the `table=True` configuration. +/// ## Define the Fields, Columns @@ -70,61 +63,41 @@ The name of each of these variables will be the name of the column in the table. And the type of each of them will also be the type of table column: -```Python hl_lines="1 3 7-10" -{!./docs_src/tutorial/create_db_and_table/tutorial001.py[ln:1-10]!} - -# More code here later 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/create_db_and_table/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/create_db_and_table/tutorial001_py310.py ln[1:8] hl[1,5:8] *} Let's now see with more detail these field/column declarations. -### Optional Fields, Nullable Columns - -Let's start with `age`, notice that it has a type of `Optional[int]`. +### `None` Fields, Nullable Columns -And we import that `Optional` from the `typing` standard module. +Let's start with `age`, notice that it has a type of `int | None`. That is the standard way to declare that something "could be an `int` or `None`" in Python. And we also set the default value of `age` to `None`. -```Python hl_lines="1 10" -{!./docs_src/tutorial/create_db_and_table/tutorial001.py[ln:1-10]!} +{* ./docs_src/tutorial/create_db_and_table/tutorial001_py310.py ln[1:8] hl[8] *} -# More code here later 👇 -``` +/// tip -
-👀 Full file preview +We also define `id` with `int | None`. But we will talk about `id` below. -```Python -{!./docs_src/tutorial/create_db_and_table/tutorial001.py!} -``` +/// -
+Because the type is `int | None`: -!!! tip - We also define `id` with `Optional`. But we will talk about `id` below. +* When validating data, `None` will be an allowed value for `age`. +* In the database, the column for `age` will be allowed to have `NULL` (the SQL equivalent to Python's `None`). -This way, we tell **SQLModel** that `age` is not required when validating data and that it has a default value of `None`. +And because there's a default value `= None`: -And we also tell it that, in the SQL database, the default value of `age` is `NULL` (the SQL equivalent to Python's `None`). +* When validating data, this `age` field won't be required, it will be `None` by default. +* When saving to the database, the `age` column will have a `NULL` value by default. -So, this column is "nullable" (can be set to `NULL`). +/// tip -!!! info - In terms of **Pydantic**, `age` is an **optional field**. +The default value could have been something else, like `= 42`. - In terms of **SQLAlchemy**, `age` is a **nullable column**. +/// ### Primary Key `id` @@ -134,24 +107,11 @@ So, we need to mark `id` as the **primary key**. To do that, we use the special `Field` function from `sqlmodel` and set the argument `primary_key=True`: -```Python hl_lines="3 7" -{!./docs_src/tutorial/create_db_and_table/tutorial001.py[ln:1-10]!} - -# More code here later 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/create_db_and_table/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/create_db_and_table/tutorial001_py310.py ln[1:8] hl[1,5] *} That way, we tell **SQLModel** that this `id` field/column is the primary key of the table. -But inside the SQL database, it is **always required** and can't be `NULL`. Why should we declare it with `Optional`? +But inside the SQL database, it is **always required** and can't be `NULL`. Why should we declare it with `int | None`? The `id` will be required in the database, but it will be *generated by the database*, not by our code. @@ -168,7 +128,7 @@ somehow_save_in_db(my_hero) do_something(my_hero.id) # Now my_hero.id has a value generated in DB 🎉 ``` -So, because in *our code* (not in the database) the value of `id` *could be* `None`, we use `Optional`. This way **the editor will be able to help us**, for example, if we try to access the `id` of an object that we haven't saved in the database yet and would still be `None`. +So, because in *our code* (not in the database) the value of `id` *could be* `None`, we use `int | None`. This way **the editor will be able to help us**, for example, if we try to access the `id` of an object that we haven't saved in the database yet and would still be `None`. @@ -190,54 +150,31 @@ If you have a server database (for example PostgreSQL or MySQL), the **engine** Creating the **engine** is very simple, just call `create_engine()` with a URL for the database to use: -```Python hl_lines="3 16" -{!./docs_src/tutorial/create_db_and_table/tutorial001.py[ln:1-16]!} - -# More code here later 👇 -``` +{* ./docs_src/tutorial/create_db_and_table/tutorial001_py310.py ln[1:16] hl[1,14] *} -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/create_db_and_table/tutorial001.py!} -``` +You should normally have a single **engine** object for your whole application and re-use it everywhere. -
+/// tip -You should normally have a single **engine** object for your whole application and re-use it everywhere. +There's another related thing called a **Session** that normally should *not* be a single object per application. -!!! tip - There's another related thing called a **Session** that normally should *not* be a single object per application. +But we will talk about it later. - But we will talk about it later. +/// ### Engine Database URL -Each supported database has it's own URL type. For example, for **SQLite** it is `sqlite:///` followed by the file path. For example: +Each supported database has its own URL type. For example, for **SQLite** it is `sqlite:///` followed by the file path. For example: * `sqlite:///database.db` * `sqlite:///databases/local/application.db` * `sqlite:///db.sqlite` -For SQLAlchemy, there's also a special one, which is a database all *in memory*, this means that it is deleted after the program terminates, and it's also very fast: +SQLite supports a special database that lives all *in memory*. Hence, it's very fast, but be careful, the database gets deleted after the program terminates. You can specify this in-memory database by using just two slash characters (`//`) and no file name: * `sqlite://` -```Python hl_lines="13-14 16" -{!./docs_src/tutorial/create_db_and_table/tutorial001.py[ln:1-19]!} - -# More code here later 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/create_db_and_table/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/create_db_and_table/tutorial001_py310.py ln[1:16] hl[11:12,14] *} You can read a lot more about all the databases supported by **SQLAlchemy** (and that way supported by **SQLModel**) in the SQLAlchemy documentation. @@ -249,20 +186,7 @@ It will make the engine print all the SQL statements it executes, which can help It is particularly useful for **learning** and **debugging**: -```Python hl_lines="16" -{!./docs_src/tutorial/create_db_and_table/tutorial001.py[ln:1-16]!} - -# More code here later 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/create_db_and_table/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/create_db_and_table/tutorial001_py310.py ln[1:16] hl[14] *} But in production, you would probably want to remove `echo=True`: @@ -272,12 +196,15 @@ engine = create_engine(sqlite_url) ### Engine Technical Details -!!! tip - If you didn't know about SQLAlchemy before and are just learning **SQLModel**, you can probably skip this section, scroll below. +/// tip + +If you didn't know about SQLAlchemy before and are just learning **SQLModel**, you can probably skip this section, scroll below. + +/// You can read a lot more about the engine in the SQLAlchemy documentation. -**SQLModel** defines it's own `create_engine()` function. It is the same as SQLAlchemy's `create_engine()`, but with the difference that it defaults to use `future=True` (which means that it uses the style of the latest SQLAlchemy, 1.4, and the future 2.0). +**SQLModel** defines its own `create_engine()` function. It is the same as SQLAlchemy's `create_engine()`, but with the difference that it defaults to use `future=True` (which means that it uses the style of the latest SQLAlchemy, 1.4, and the future 2.0). And SQLModel's version of `create_engine()` is type annotated internally, so your editor will be able to help you with autocompletion and inline errors. @@ -285,16 +212,17 @@ And SQLModel's version of `create_engine()` is type annotated internally, so you Now everything is in place to finally create the database and table: -```Python hl_lines="18" -{!./docs_src/tutorial/create_db_and_table/tutorial001.py!} -``` +{* ./docs_src/tutorial/create_db_and_table/tutorial001_py310.py hl[16] *} -!!! tip - Creating the engine doesn't create the `database.db` file. +/// tip - But once we run `SQLModel.metadata.create_all(engine)`, it creates the `database.db` file **and** creates the `hero` table in that database. +Creating the engine doesn't create the `database.db` file. - Both things are done in this single step. +But once we run `SQLModel.metadata.create_all(engine)`, it creates the `database.db` file **and** creates the `hero` table in that database. + +Both things are done in this single step. + +/// Let's unwrap that: @@ -395,17 +323,13 @@ Let's run the program to see it all working. Put the code it in a file `app.py` if you haven't already. -
-👀 Full file preview +{* ./docs_src/tutorial/create_db_and_table/tutorial001_py310.py *} -```Python -{!./docs_src/tutorial/create_db_and_table/tutorial001.py!} -``` +/// tip -
+Remember to [activate the virtual environment](../virtual-environments.md#create-a-virtual-environment){.internal-link target=_blank} before running it. -!!! tip - Remember to [activate the virtual environment](./index.md#create-a-python-virtual-environment){.internal-link target=_blank} before running it. +/// Now run the program with Python: @@ -415,22 +339,22 @@ Now run the program with Python: // We set echo=True, so this will show the SQL code $ python app.py -// First, some boilerplate SQL that we are not that intereted in +// First, some boilerplate SQL that we are not that interested in INFO Engine BEGIN (implicit) INFO Engine PRAGMA main.table_info("hero") INFO Engine [raw sql] () INFO Engine PRAGMA temp.table_info("hero") INFO Engine [raw sql] () -INFO Engine +INFO Engine // Finally, the glorious SQL to create the table ✨ CREATE TABLE hero ( - id INTEGER, - name VARCHAR NOT NULL, - secret_name VARCHAR NOT NULL, - age INTEGER, + id INTEGER, + name VARCHAR NOT NULL, + secret_name VARCHAR NOT NULL, + age INTEGER, PRIMARY KEY (id) ) @@ -442,20 +366,23 @@ INFO Engine COMMIT -!!! info - I simplified the output above a bit to make it easier to read. +/// info + +I simplified the output above a bit to make it easier to read. - But in reality, instead of showing: +But in reality, instead of showing: - ``` - INFO Engine BEGIN (implicit) - ``` +``` +INFO Engine BEGIN (implicit) +``` - it would show something like: +it would show something like: + +``` +2021-07-25 21:37:39,175 INFO sqlalchemy.engine.Engine BEGIN (implicit) +``` - ``` - 2021-07-25 21:37:39,175 INFO sqlalchemy.engine.Engine BEGIN (implicit) - ``` +/// ### `TEXT` or `VARCHAR` @@ -463,7 +390,7 @@ In the example in the previous chapter we created the table using `TEXT` for som But in this output SQLAlchemy is using `VARCHAR` instead. Let's see what's going on. -Remember that [each SQL Database has some different variations in what they support?](../databases/#sql-the-language){.internal-link target=_blank} +Remember that [each SQL Database has some different variations in what they support?](../databases.md#sql-the-language){.internal-link target=_blank} This is one of the differences. Each database supports some particular **data types**, like `INTEGER` and `TEXT`. @@ -479,8 +406,11 @@ Additional to the difference between those two data types, some databases like M To make it easier to start using **SQLModel** right away independent of the database you use (even with MySQL), and without any extra configurations, by default, `str` fields are interpreted as `VARCHAR` in most databases and `VARCHAR(255)` in MySQL, this way you know the same class will be compatible with the most popular databases without extra effort. -!!! tip - You will learn how to change the maximum length of string columns later in the Advanced Tutorial - User Guide. +/// tip + +You will learn how to change the maximum length of string columns later in the Advanced Tutorial - User Guide. + +/// ### Verify the Database @@ -498,46 +428,37 @@ In this example it's just the `SQLModel.metadata.create_all(engine)`. Let's put it in a function `create_db_and_tables()`: -```Python hl_lines="22-23" -{!./docs_src/tutorial/create_db_and_table/tutorial002.py[ln:1-20]!} - -# More code here later 👇 -``` - -
-👀 Full file preview +{* ./docs_src/tutorial/create_db_and_table/tutorial002_py310.py ln[1:18] hl[17:18] *} -```Python -{!./docs_src/tutorial/create_db_and_table/tutorial002.py!} -``` +If `SQLModel.metadata.create_all(engine)` was not in a function and we tried to import something from this module (from this file) in another, it would try to create the database and table **every time** we executed that other file that imported this module. -
+We don't want that to happen like that, only when we **intend** it to happen, that's why we put it in a function, because we can make sure that the tables are created only when we call that function, and not when this module is imported somewhere else. -If `SQLModel.metadata.create_all(engine)` was not in a function and we tried to import something from this module (from this file) in another, it would try to create the database and table **every time**. +Now we would be able to, for example, import the `Hero` class in some other file without having those **side effects**. -We don't want that to happen like that, only when we **intend** it to happen, that's why we put it in a function. +/// tip -Now we would be able to, for example, import the `Hero` class in some other file without having those **side effects**. +😅 **Spoiler alert**: The function is called `create_db_and_tables()` because we will have more **tables** in the future with other classes apart from `Hero`. 🚀 -!!! tip - 😅 **Spoiler alert**: The function is called `create_db_and_tables()` because we will have more **tables** in the future with other classes apart from `Hero`. 🚀 +/// ### Create Data as a Script We prevented the side effects when importing something from your `app.py` file. -But we still want it to **create the database and table** when we call it with Python directly as an independent script from the terminal, just as as above. +But we still want it to **create the database and table** when we call it with Python directly as an independent script from the terminal, just as above. + +/// tip + +Think of the word **script** and **program** as interchangeable. -!!! tip - Think of the word **script** and **program** as interchangeable. +The word **script** often implies that the code could be run independently and easily. Or in some cases it refers to a relatively simple program. - The word **script** often implies that the code could be run independently and easily. Or in some cases it refers to a relatively simple program. +/// For that we can use the special variable `__name__` in an `if` block: -```Python hl_lines="23-24" -{!./docs_src/tutorial/create_db_and_table/tutorial002.py!} -``` +{* ./docs_src/tutorial/create_db_and_table/tutorial002_py310.py hl[21:22] *} ### About `__name__ == "__main__"` @@ -559,10 +480,13 @@ $ python app.py from app import Hero ``` -!!! tip - That `if` block using `if __name__ == "__main__":` is sometimes called the "**main block**". +/// tip + +That `if` block using `if __name__ == "__main__":` is sometimes called the "**main block**". + +The official name (in the Python docs) is "**Top-level script environment**". - The official name (in the Python docs) is "**Top-level script environment**". +/// #### More details @@ -614,8 +538,11 @@ if __name__ == "__main__": ...will **not** be executed. -!!! info - For more information, check the official Python docs. +/// info + +For more information, check the official Python docs. + +/// ## Last Review @@ -625,14 +552,31 @@ But now we can import things from this module in other files. Now, let's give the code a final look: +//// tab | Python 3.10+ + ```{.python .annotate} -{!./docs_src/tutorial/create_db_and_table/tutorial003.py!} +{!./docs_src/tutorial/create_db_and_table/tutorial003_py310.py!} ``` {!./docs_src/tutorial/create_db_and_table/annotations/en/tutorial003.md!} -!!! tip - Review what each line does by clicking each number bubble in the code. 👆 +//// + +//// tab | Python 3.9+ + +```{.python .annotate} +{!./docs_src/tutorial/create_db_and_table/tutorial003_py39.py!} +``` + +{!./docs_src/tutorial/create_db_and_table/annotations/en/tutorial003.md!} + +//// + +/// tip + +Review what each line does by clicking each number bubble in the code. 👆 + +/// ## Recap diff --git a/docs/tutorial/delete.md b/docs/tutorial/delete.md index 0c9238d018..9f494ec44c 100644 --- a/docs/tutorial/delete.md +++ b/docs/tutorial/delete.md @@ -6,14 +6,7 @@ Now let's delete some data using **SQLModel**. As before, we'll continue from where we left off with the previous code. -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/update/tutorial003.py!} -``` - -
+{* ./docs_src/tutorial/update/tutorial003_py310.py ln[0] *} Remember to remove the `database.db` file before running the examples to get the same results. @@ -63,39 +56,11 @@ To get the same results, delete the `database.db` file before running the exampl We'll start by selecting the hero `"Spider-Youngster"` that we updated in the previous chapter, this is the one we will delete: -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/delete/tutorial001.py[ln:72-77]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/delete/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/delete/tutorial001_py310.py ln[70:75] hl[72] *} As this is a new function `delete_heroes()`, we'll also add it to the `main()` function so that we call it when executing the program from the command line: -```Python hl_lines="7" -# Code above omitted 👆 - -{!./docs_src/tutorial/delete/tutorial001.py[ln:92-100]!} -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/delete/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/delete/tutorial001_py310.py ln[90:98] hl[94] *} That will print the same existing hero **Spider-Youngster**: @@ -108,8 +73,8 @@ $ python app.py // The SELECT with WHERE INFO Engine BEGIN (implicit) -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.name = ? INFO Engine [no key 0.00011s] ('Spider-Youngster',) @@ -123,22 +88,7 @@ Hero: name='Spider-Youngster' secret_name='Pedro Parqueador' age=16 id=2 Now, very similar to how we used `session.add()` to add or update new heroes, we can use `session.delete()` to delete the hero from the session: -```Python hl_lines="10" -# Code above omitted 👆 - -{!./docs_src/tutorial/delete/tutorial001.py[ln:72-79]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/delete/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/delete/tutorial001_py310.py ln[70:77] hl[77] *} ## Commit the Session @@ -146,22 +96,7 @@ To save the current changes in the session, **commit** it. This will save all the changes stored in the **session**, like the deleted hero: -```Python hl_lines="11" -# Code above omitted 👆 - -{!./docs_src/tutorial/delete/tutorial001.py[ln:72-80]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/delete/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/delete/tutorial001_py310.py ln[70:78] hl[78] *} The same as we have seen before, `.commit()` will also save anything else that was added to the session. Including updates, or created heroes. @@ -196,22 +131,7 @@ As the object is not connected to the session, it is not marked as "expired", th Because of that, the object still contains its attributes with the data in it, so we can print it: -```Python hl_lines="13" -# Code above omitted 👆 - -{!./docs_src/tutorial/delete/tutorial001.py[ln:72-82]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/delete/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/delete/tutorial001_py310.py ln[70:80] hl[80] *} This will output: @@ -234,22 +154,7 @@ Deleted hero: name='Spider-Youngster' secret_name='Pedro Parqueador' age=16 id=2 To confirm if it was deleted, now let's query the database again, with the same `"Spider-Youngster"` name: -```Python hl_lines="15-17" -# Code above omitted 👆 - -{!./docs_src/tutorial/delete/tutorial001.py[ln:72-86]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/delete/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/delete/tutorial001_py310.py ln[70:84] hl[82:84] *} Here we are using `results.first()` to get the first object found (in case it found multiple) or `None`, if it didn't find anything. @@ -272,8 +177,8 @@ $ python app.py INFO Engine BEGIN (implicit) // SQL to search for the hero -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.name = ? INFO Engine [no key 0.00013s] ('Spider-Youngster',) ``` @@ -286,22 +191,7 @@ Now let's just confirm that, indeed, no hero was found in the database with that We'll do it by checking that the "first" item in the `results` is `None`: -```Python hl_lines="19-20" -# Code above omitted 👆 - -{!./docs_src/tutorial/delete/tutorial001.py[ln:72-89]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/delete/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/delete/tutorial001_py310.py ln[70:87] hl[86:87] *} This will output: @@ -327,14 +217,31 @@ INFO Engine ROLLBACK Now let's review all that code: +//// tab | Python 3.10+ + +```{ .python .annotate hl_lines="70-88" } +{!./docs_src/tutorial/delete/tutorial002_py310.py!} +``` + +{!./docs_src/tutorial/delete/annotations/en/tutorial002.md!} + +//// + +//// tab | Python 3.9+ + ```{ .python .annotate hl_lines="72-90" } -{!./docs_src/tutorial/delete/tutorial002.py!} +{!./docs_src/tutorial/delete/tutorial002_py39.py!} ``` {!./docs_src/tutorial/delete/annotations/en/tutorial002.md!} -!!! tip - Check out the number bubbles to see what is done by each line of code. +//// + +/// tip + +Check out the number bubbles to see what is done by each line of code. + +/// ## Recap diff --git a/docs/tutorial/fastapi/delete.md b/docs/tutorial/fastapi/delete.md index 2ce3fe5b8b..c7339ef384 100644 --- a/docs/tutorial/fastapi/delete.md +++ b/docs/tutorial/fastapi/delete.md @@ -12,22 +12,7 @@ We get a `hero_id` from the path parameter and verify if it exists, just as we d And if we actually find a hero, we just delete it with the **session**. -```Python hl_lines="3-11" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/delete/tutorial001.py[ln:91-99]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/fastapi/delete/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/fastapi/delete/tutorial001_py310.py ln[89:97] hl[89:97] *} After deleting it successfully, we just return a response of: @@ -39,6 +24,6 @@ After deleting it successfully, we just return a response of: ## Recap -That's it, feel free to try it out in the interactve docs UI to delete some heroes. 💥 +That's it, feel free to try it out in the interactive docs UI to delete some heroes. 💥 Using **FastAPI** to read data and combining it with **SQLModel** makes it quite straightforward to delete data from the database. diff --git a/docs/tutorial/fastapi/limit-and-offset.md b/docs/tutorial/fastapi/limit-and-offset.md index 57043ceaf7..1a464a664f 100644 --- a/docs/tutorial/fastapi/limit-and-offset.md +++ b/docs/tutorial/fastapi/limit-and-offset.md @@ -2,14 +2,17 @@ When a client sends a request to get all the heroes, we have been returning them all. -But if we had **thousands** of heroes that could consume a lot of **computational resources**, network bandwith, etc. +But if we had **thousands** of heroes that could consume a lot of **computational resources**, network bandwidth, etc. -So we probably want to limit it. +So, we probably want to limit it. Let's use the same **offset** and **limit** we learned about in the previous tutorial chapters for the API. -!!! info - In many cases this is also called **pagination**. +/// info + +In many cases, this is also called **pagination**. + +/// ## Add a Limit and Offset to the Query Parameters @@ -19,39 +22,25 @@ By default, we will return the first results from the database, so `offset` will And by default, we will return a maximum of `100` heroes, so `limit` will have a default value of `100`. -```Python hl_lines="3 9 11" -{!./docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py[ln:1-4]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py[ln:54-58]!} +{* ./docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py310.py ln[1:2,52:56] hl[1,53,55] *} -# Code below omitted 👇 -``` +We want to allow clients to set different `offset` and `limit` values. -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py!} -``` - -
+But we don't want them to be able to set a `limit` of something like `9999`, that's over `9000`! 😱 -We want to allow clients to set a different `offset` and `limit` values. +So, to prevent it, we add additional validation to the `limit` query parameter, declaring that it has to be **l**ess than or **e**qual to `100` with `le=100`. -But we don't want them to be able to set a `limit` of something like `9999`, that's over `9000`! 😱 +This way, a client can decide to take fewer heroes if they want, but not more. -So, to prevent it, we add additional validation to the `limit` query parameter, declaring that it has to be **l**ess **t**han or **e**qual to `100` with `lte=100`. +/// info -This way, a client can decide to take less heroes if they want, but not more. +If you need to refresh how query parameters and their validation work, check out the docs in FastAPI: -!!! info - If you need to refresh how query parameters and their validation work, check out the docs in FastAPI: +* Query Parameters +* Query Parameters and String Validations +* Path Parameters and Numeric Validations - * Query Parameters - * Query Parameters and String Validations - * Path Parameters and Numeric Validations +/// ## Check the Docs UI diff --git a/docs/tutorial/fastapi/multiple-models.md b/docs/tutorial/fastapi/multiple-models.md index d313874c98..1bc045612b 100644 --- a/docs/tutorial/fastapi/multiple-models.md +++ b/docs/tutorial/fastapi/multiple-models.md @@ -2,7 +2,7 @@ We have been using the same `Hero` model to declare the schema of the data we receive in the API, the table model in the database, and the schema of the data we send back in responses. -But in most of the cases there are slight differences, let's use multiple models to solve it. +But in most of the cases, there are slight differences. Let's use multiple models to solve it. Here you will see the main and biggest feature of **SQLModel**. 😎 @@ -10,17 +10,17 @@ Here you will see the main and biggest feature of **SQLModel**. 😎 Let's start by reviewing the automatically generated schemas from the docs UI. -For input we have: +For input, we have: Interactive API docs UI If we pay attention, it shows that the client *could* send an `id` in the JSON body of the request. -This means that the client could try to use the same ID that already exists in the database for another hero. +This means that the client could try to use the same ID that already exists in the database to create another hero. That's not what we want. -We want the client to only send the data that is needed to create a new hero: +We want the client only to send the data that is needed to create a new hero: * `name` * `secret_name` @@ -51,19 +51,19 @@ The `age` is optional, we don't have to return it, or it could be `None` (or `nu Here's the weird thing, the `id` currently seems also "optional". 🤔 -This is because in our **SQLModel** class we declare the `id` with `Optional[int]`, because it could be `None` in memory until we save it in the database and we finally get the actual ID. +This is because in our **SQLModel** class we declare the `id` with a default value of `= None`, because it could be `None` in memory until we save it in the database and we finally get the actual ID. -But in the responses, we would always send a model from the database, and it would **always have an ID**. So the `id` in the responses could be declared as required too. +But in the responses, we always send a model from the database, so it **always has an ID**. So the `id` in the responses can be declared as required. -This would mean that our application is making the compromise with the clients that if it sends a hero, it would for sure have an `id` with a value, it would not be `None`. +This means that our application is making the promise to the clients that if it sends a hero, it will for sure have an `id` with a value, it will not be `None`. -### Why Is it Important to Compromise with the Responses +### Why Is it Important to Have a Contract for Responses The ultimate goal of an API is for some **clients to use it**. The clients could be a frontend application, a command line program, a graphical user interface, a mobile application, another backend application, etc. -And the code those clients write depend on what our API tells them they **need to send**, and what they can **expect to receive**. +And the code those clients write depends on what our API tells them they **need to send**, and what they can **expect to receive**. Making both sides very clear will make it much easier to interact with the API. @@ -71,7 +71,7 @@ And in most of the cases, the developer of the client for that API **will also b ### So Why is it Important to Have Required IDs -Now, what's the matter with having one **`id` field marked as "optional"** in a response when in reality it is always required? +Now, what's the matter with having one **`id` field marked as "optional"** in a response when in reality it is always available (required)? For example, **automatically generated clients** in other languages (or also in Python) would have some declaration that this field `id` is optional. @@ -98,7 +98,7 @@ But we also want to have a `HeroCreate` for the data we want to receive when **c * `secret_name`, required * `age`, optional -And we want to have a `HeroRead` with the `id` field, but this time annotated with `id: int`, instead of `id: Optional[int]`, to make it clear that it is required in responses **read** from the clients: +And we want to have a `HeroPublic` with the `id` field, but this time with a type of `id: int`, instead of `id: int | None`, to make it clear that it will always have an `int` in responses **read** from the clients: * `id`, required * `name`, required @@ -109,35 +109,21 @@ And we want to have a `HeroRead` with the `id` field, but this time annotated wi The simplest way to solve it could be to create **multiple models**, each one with all the corresponding fields: -```Python hl_lines="5-9 12-15 18-22" -# This would work, but there's a better option below 🚨 - -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/multiple_models/tutorial001.py[ln:7-24]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/fastapi/multiple_models/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py ln[5:22] hl[5:9,12:15,18:22] *} Here's the important detail, and probably the most important feature of **SQLModel**: only `Hero` is declared with `table = True`. This means that the class `Hero` represents a **table** in the database. It is both a **Pydantic** model and a **SQLAlchemy** model. -But `HeroCreate` and `HeroRead` don't have `table = True`. They are only **data models**, they are only **Pydantic** models. They won't be used with the database, but only to declare data schemas for the API (or for other uses). +But `HeroCreate` and `HeroPublic` don't have `table = True`. They are only **data models**, they are only **Pydantic** models. They won't be used with the database, but only to declare data schemas for the API (or for other uses). -This also means that `SQLModel.metadata.create_all()` won't create tables in the database for `HeroCreate` and `HeroRead`, because they don't have `table = True`, which is exactly what we want. 🚀 +This also means that `SQLModel.metadata.create_all()` won't create tables in the database for `HeroCreate` and `HeroPublic`, because they don't have `table = True`, which is exactly what we want. 🚀 -!!! tip - We will improve this code to avoid duplicating the fields, but for now we can continue learning with these models. +/// tip + +We will improve this code to avoid duplicating the fields, but for now we can continue learning with these models. + +/// ## Use Multiple Models to Create a Hero @@ -145,91 +131,63 @@ Let's now see how to use these new models in the FastAPI application. Let's first check how is the process to create a hero now: -```Python hl_lines="3-4 6" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/multiple_models/tutorial001.py[ln:46-53]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/fastapi/multiple_models/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py ln[44:51] hl[44:45,47] *} Let's check that in detail. -Now we use the type annotation `HeroCreate` for the request JSON data, in the `hero` parameter of the **path operation function**. - -```Python hl_lines="3" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/multiple_models/tutorial001.py[ln:47]!} - -# Code below omitted 👇 -``` +Now we use the type annotation `HeroCreate` for the request JSON data in the `hero` parameter of the **path operation function**. -Then we create a new `Hero` (this is the actual **table** model that saves things to the database) using `Hero.from_orm()`. +{* ./docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py ln[45] hl[45] *} -The method `.from_orm()` reads data from another object with attributes and creates a new instance of this class, in this case `Hero`. +Then we create a new `Hero` (this is the actual **table** model that saves things to the database) using `Hero.model_validate()`. -The alternative is `Hero.parse_obj()` that reads data from a dictionary. +The method `.model_validate()` reads data from another object with attributes (or a dict) and creates a new instance of this class, in this case `Hero`. -But as in this case we have a `HeroCreate` instance in the `hero` variable, this is an object with attributes, so we use `.from_orm()` to read those attributes. +In this case, we have a `HeroCreate` instance in the `hero` variable. This is an object with attributes, so we use `.model_validate()` to read those attributes. -With this we create a new `Hero` instance (the one for the database) and put it in the variable `db_hero` from the data in the `hero` variable that is the `HeroCreate` instance we received from the request. +/// tip +In versions of **SQLModel** before `0.0.14` you would use the method `.from_orm()`, but it is now deprecated and you should use `.model_validate()` instead. +/// -```Python hl_lines="3" -# Code above omitted 👆 +We can now create a new `Hero` instance (the one for the database) and put it in the variable `db_hero` from the data in the `hero` variable that is the `HeroCreate` instance we received from the request. -{!./docs_src/tutorial/fastapi/multiple_models/tutorial001.py[ln:49]!} +{* ./docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py ln[47] hl[47] *} -# Code below omitted 👇 -``` - -Then we just `add` it to the **session**, `commit`, and `refresh` it, and finally we return the same `db_hero` variable that has the just refreshed `Hero` instance. +Then we just `add` it to the **session**, `commit`, and `refresh` it, and finally, we return the same `db_hero` variable that has the just refreshed `Hero` instance. Because it is just refreshed, it has the `id` field set with a new ID taken from the database. -And now that we return it, FastAPI will validate the data with the `response_model`, which is a `HeroRead`: +And now that we return it, FastAPI will validate the data with the `response_model`, which is a `HeroPublic`: -```Python hl_lines="3" -# Code above omitted 👆 +{* ./docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py ln[44] hl[44] *} -{!./docs_src/tutorial/fastapi/multiple_models/tutorial001.py[ln:46]!} +This will validate that all the data that we promised is there and will remove any data we didn't declare. -# Code below omitted 👇 -``` +/// tip -This will validate that all the data that we promised is there, and will remove any data we didn't declare. +This filtering could be very important and could be a very good security feature, for example, to make sure you filter private data, hashed passwords, etc. -!!! tip - This filtering could be very important, and could be a very good security feature, for example to make sure you filter private data, hashed passwords, etc. +You can read more about it in the FastAPI docs about Response Model. - You can read more about it in the FastAPI docs about Response Model. +/// -In particular, it will make sure that the `id` is there, and that it is indeed an integer (and not `None`). +In particular, it will make sure that the `id` is there and that it is indeed an integer (and not `None`). ## Shared Fields But looking closely, we could see that these models have a lot of **duplicated information**. -All **the 3 models** declare that thay share some **common fields** that look exactly the same: +All **the 3 models** declare that they share some **common fields** that look exactly the same: * `name`, required * `secret_name`, required * `age`, optional -And then they declare other fields with some differences (in this case only about the `id`). +And then they declare other fields with some differences (in this case, only about the `id`). We want to **avoid duplicated information** if possible. -This is important if, for example, in the future we decide to **refactor the code** and rename one field (column). For example, from `secret_name` to `secret_identity`. +This is important if, for example, in the future, we decide to **refactor the code** and rename one field (column). For example, from `secret_name` to `secret_identity`. If we have that duplicated in multiple models, we could easily forget to update one of them. But if we **avoid duplication**, there's only one place that would need updating. ✨ @@ -253,22 +211,7 @@ We can see from above that they all share some **base** fields: So let's create a **base** model `HeroBase` that the others can inherit from: -```Python hl_lines="3-6" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py[ln:7-10]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py!} -``` - -
+{* ./docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py ln[5:8] hl[5:8] *} As you can see, this is *not* a **table model**, it doesn't have the `table = True` config. @@ -278,26 +221,11 @@ But now we can create the **other models inheriting from it**, they will all sha Let's start with the only **table model**, the `Hero`: -```Python hl_lines="9-10" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py[ln:7-14]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py!} -``` - -
+{* ./docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py ln[5:12] hl[11:12] *} Notice that `Hero` now doesn't inherit from `SQLModel`, but from `HeroBase`. -And now we only declare one single field directly, the `id`, that here is `Optional[int]`, and is a `primary_key`. +And now we only declare one single field directly, the `id`, that here is `int | None`, and is a `primary_key`. And even though we don't declare the other fields **explicitly**, because they are inherited, they are also part of this `Hero` model. @@ -309,22 +237,7 @@ And those inherited fields will also be in the **autocompletion** and **inline e Notice that the parent model `HeroBase` is not a **table model**, but still, we can declare `name` and `age` using `Field(index=True)`. -```Python hl_lines="4 6 9" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py[ln:7-14]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py!} -``` - -
+{* ./docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py ln[5:12] hl[6,8,11] *} This won't affect this parent **data model** `HeroBase`. @@ -336,22 +249,7 @@ Now let's see the `HeroCreate` model that will be used to define the data that w This is a fun one: -```Python hl_lines="13-14" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py[ln:7-18]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py!} -``` - -
+{* ./docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py ln[5:16] hl[15:16] *} What's happening here? @@ -361,36 +259,21 @@ And because we can't leave the empty space when creating a new class, but we don This means that there's nothing else special in this class apart from the fact that it is named `HeroCreate` and that it inherits from `HeroBase`. -As an alternative, we could use `HeroBase` directly in the API code instead of `HeroCreate`, but it would show up in the auomatic docs UI with that name "`HeroBase`" which could be **confusing** for clients. Instead, "`HeroCreate`" is a bit more explicit about what it is for. +As an alternative, we could use `HeroBase` directly in the API code instead of `HeroCreate`, but it would show up in the automatic docs UI with that name "`HeroBase`" which could be **confusing** for clients. Instead, "`HeroCreate`" is a bit more explicit about what it is for. -On top of that, we could easily decide in the future that we want to receive **more data** when creating a new hero apart from the data in `HeroBase` (for example a password), and now we already have the class to put those extra fields. +On top of that, we could easily decide in the future that we want to receive **more data** when creating a new hero apart from the data in `HeroBase` (for example, a password), and now we already have the class to put those extra fields. -### The `HeroRead` **Data Model** +### The `HeroPublic` **Data Model** -Now let's check the `HeroRead` model. +Now let's check the `HeroPublic` model. This one just declares that the `id` field is required when reading a hero from the API, because a hero read from the API will come from the database, and in the database it will always have an ID. -```Python hl_lines="17-18" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py[ln:7-22]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py!} -``` - -
+{* ./docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py ln[5:20] hl[19:20] *} ## Review the Updated Docs UI -The FastAPI code is still the same as above, we still use `Hero`, `HeroCreate`, and `HeroRead`. But now we define them in a smarter way with inheritance. +The FastAPI code is still the same as above, we still use `Hero`, `HeroCreate`, and `HeroPublic`. But now, we define them in a smarter way with inheritance. So, we can jump to the docs UI right away and see how they look with the updated data. @@ -400,7 +283,7 @@ Let's see the new UI for creating a hero: Interactive API docs UI -Nice! It now shows that to create a hero, we just pass the `name`, `secret_name`, and optinally `age`. +Nice! It now shows that to create a hero, we just pass the `name`, `secret_name`, and optionally `age`. We no longer pass an `id`. @@ -416,7 +299,7 @@ And if we check the schema for the **Read Heroes** *path operation* it will also ## Inheritance and Table Models -We just saw how powerful inheritance of these models can be. +We just saw how powerful the inheritance of these models could be. This is a very simple example, and it might look a bit... meh. 😅 diff --git a/docs/tutorial/fastapi/read-one.md b/docs/tutorial/fastapi/read-one.md index b503546298..03a65a0a2d 100644 --- a/docs/tutorial/fastapi/read-one.md +++ b/docs/tutorial/fastapi/read-one.md @@ -8,25 +8,13 @@ Let's add a new *path operation* to read one single hero. We want to get the hero based on the `id`, so we will use a **path parameter** `hero_id`. -!!! info - If you need to refresh how *path parameters* work, including their data validation, check the FastAPI docs about Path Parameters. +/// info -```Python hl_lines="8" -{!./docs_src/tutorial/fastapi/read_one/tutorial001.py[ln:1-4]!} +If you need to refresh how *path parameters* work, including their data validation, check the FastAPI docs about Path Parameters. -# Code here omitted 👈 +/// -{!./docs_src/tutorial/fastapi/read_one/tutorial001.py[ln:61-67]!} -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/fastapi/read_one/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/fastapi/read_one/tutorial001_py310.py ln[1:2,59:65] hl[59] *} For example, to get the hero with ID `2` we would send a `GET` request to: @@ -42,49 +30,19 @@ But if the integer is not the ID of any hero in the database, it will not find a So, we check it in an `if` block, if it's `None`, we raise an `HTTPException` with a `404` status code. -And to use it we first import `HTTPException` from `fastapi`. +And to use it, we first import `HTTPException` from `fastapi`. This will let the client know that they probably made a mistake on their side and requested a hero that doesn't exist in the database. -```Python hl_lines="3 11-13" -{!./docs_src/tutorial/fastapi/read_one/tutorial001.py[ln:1-4]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/read_one/tutorial001.py[ln:61-67]!} -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/fastapi/read_one/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/fastapi/read_one/tutorial001_py310.py ln[1:2,59:65] hl[1,62:64] *} ## Return the Hero Then, if the hero exists, we return it. -And because we are using the `response_model` with `HeroRead`, it will be validated, documented, etc. - -```Python hl_lines="8 14" -{!./docs_src/tutorial/fastapi/read_one/tutorial001.py[ln:1-4]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/read_one/tutorial001.py[ln:61-67]!} -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/fastapi/read_one/tutorial001.py!} -``` +And because we are using the `response_model` with `HeroPublic`, it will be validated, documented, etc. -
+{* ./docs_src/tutorial/fastapi/read_one/tutorial001_py310.py ln[1:2,59:65] hl[59,65] *} ## Check the Docs UI diff --git a/docs/tutorial/fastapi/relationships.md b/docs/tutorial/fastapi/relationships.md index 3aa8863f2f..f789fd930a 100644 --- a/docs/tutorial/fastapi/relationships.md +++ b/docs/tutorial/fastapi/relationships.md @@ -40,69 +40,23 @@ Let's update that. 🤓 First, why is it that we are not getting the related data for each hero and for each team? -It's because we declared the `HeroRead` with only the same base fields of the `HeroBase` plus the `id`. But it doesn't include a field `team` for the **relationship attribute**. +It's because we declared the `HeroPublic` with only the same base fields of the `HeroBase` plus the `id`. But it doesn't include a field `team` for the **relationship attribute**. -And the same way, we declared the `TeamRead` with only the same base fields of the `TeamBase` plus the `id`. But it doesn't include a field `heroes` for the **relationship attribute**. +And the same way, we declared the `TeamPublic` with only the same base fields of the `TeamBase` plus the `id`. But it doesn't include a field `heroes` for the **relationship attribute**. -```Python hl_lines="3-5 9-10 14-19 23-24" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:7-9]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:22-23]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:32-37]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:46-47]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/fastapi/teams/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/fastapi/teams/tutorial001_py310.py ln[5:7,20:21,29:34,43:44] hl[5:7,20:21,29:34,43:44] *} Now, remember that FastAPI uses the `response_model` to validate and **filter** the response data? -In this case, we used `response_model=TeamRead` and `response_model=HeroRead`, so FastAPI will use them to filter the response data, even if we return a **table model** that includes **relationship attributes**: - -```Python hl_lines="3 8 12 17" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:105-110]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:160-165]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/fastapi/teams/tutorial001.py!} -``` +In this case, we used `response_model=TeamPublic` and `response_model=HeroPublic`, so FastAPI will use them to filter the response data, even if we return a **table model** that includes **relationship attributes**: -
+{* ./docs_src/tutorial/fastapi/teams/tutorial001_py310.py ln[102:107,155:160] hl[102,107,155,160] *} ## Don't Include All the Data Now let's stop for a second and think about it. -We cannot simply include *all* the data including all the internal relationships, because each **hero** has an attribute `team` with their team, and then that **team** also has an attribute `heroes` with all the **heroes** in the team, including this one. +We cannot simply include *all* the data, including all the internal relationships, because each **hero** has an attribute `team` with their team, and then that **team** also has an attribute `heroes` with all the **heroes** in the team, including this one. If we tried to include everything, we could make the server application **crash** trying to extract **infinite data**, going through the same hero and team over and over again internally, something like this: @@ -152,7 +106,7 @@ If we tried to include everything, we could make the server application **crash* } ``` -As you can see, in this example we would get the hero **Rusty-Man**, and from this hero we would get the team **Preventers**, and then from this team we would get its heroes, of course, including **Rusty-Man**... 😱 +As you can see, in this example, we would get the hero **Rusty-Man**, and from this hero we would get the team **Preventers**, and then from this team we would get its heroes, of course, including **Rusty-Man**... 😱 So we start again, and in the end, the server would just crash trying to get all the data with a `"Maximum recursion error"`, we would not even get a response like the one above. @@ -164,7 +118,7 @@ This is a decision that will depend on **each application**. In our case, let's say that if we get a **list of heroes**, we don't want to also include each of their teams in each one. -And if we get a **list of teams**, we don't want to get a a list of the heroes for each one. +And if we get a **list of teams**, we don't want to get a list of the heroes for each one. But if we get a **single hero**, we want to include the team data (without the team's heroes). @@ -174,36 +128,21 @@ Let's add a couple more **data models** that declare that data so we can use the ## Models with Relationships -Let's add the models `HeroReadWithTeam` and `TeamReadWithHeroes`. +Let's add the models `HeroPublicWithTeam` and `TeamPublicWithHeroes`. We'll add them **after** the other models so that we can easily reference the previous models. -```Python hl_lines="3-4 7-8" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/relationships/tutorial001.py[ln:61-66]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/fastapi/relationships/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/fastapi/relationships/tutorial001_py310.py ln[59:64] hl[59:60,63:64] *} -These two models are very **simple in code**, but there's a lot happening here, let's check it out. +These two models are very **simple in code**, but there's a lot happening here. Let's check it out. ### Inheritance and Type Annotations -The `HeroReadWithTeam` **inherits** from `HeroRead`, which means that it will have the **normal fields for reading**, including the required `id` that was declared in `HeroRead`. +The `HeroPublicWithTeam` **inherits** from `HeroPublic`, which means that it will have the **normal fields for reading**, including the required `id` that was declared in `HeroPublic`. -And then it adds the **new field** `team`, which could be `None`, and is declared with the type `TeamRead` with the base fields for reading a team. +And then it adds the **new field** `team`, which could be `None`, and is declared with the type `TeamPublic` with the base fields for reading a team. -Then we do the same for the `TeamReadWithHeroes`, it **inherits** from `TeamRead`, and declare the **new field** `heroes` which is a list of `HeroRead`. +Then we do the same for the `TeamPublicWithHeroes`, it **inherits** from `TeamPublic`, and declares the **new field** `heroes`, which is a list of `HeroPublic`. ### Data Models Without Relationship Attributes @@ -213,11 +152,11 @@ Instead, here these are only **data models** that will tell FastAPI **which attr ### Reference to Other Models -Also notice that the field `team` is not declared with this new `TeamReadWithHeroes`, because that would again create that infinite recursion of data. Instead, we declare it with the normal `TeamRead` model. +Also, notice that the field `team` is not declared with this new `TeamPublicWithHeroes`, because that would again create that infinite recursion of data. Instead, we declare it with the normal `TeamPublic` model. -And the same for `TeamReadWithHeroes`, the model used for the new field `heroes` uses `HeroRead` to get only each hero's data. +And the same for `TeamPublicWithHeroes`, the model used for the new field `heroes` uses `HeroPublic` to get only each hero's data. -This also means that, even though we have these two new models, **we still need the previous ones**, `HeroRead` and `TeamRead`, because we need to reference them here (and we are also using them in the rest of the *path operations*). +This also means that, even though we have these two new models, **we still need the previous ones**, `HeroPublic` and `TeamPublic`, because we need to reference them here (and we are also using them in the rest of the *path operations*). ## Update the Path Operations @@ -227,26 +166,7 @@ This will tell **FastAPI** to take the object that we return from the *path oper In the case of the hero, this tells FastAPI to extract the `team` too. And in the case of the team, to extract the list of `heroes` too. -```Python hl_lines="3 8 12 17" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/relationships/tutorial001.py[ln:113-118]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/relationships/tutorial001.py[ln:168-173]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/fastapi/relationships/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/fastapi/relationships/tutorial001_py310.py ln[111:116,164:169] hl[111,116,164,169] *} ## Check It Out in the Docs UI @@ -267,7 +187,7 @@ Now we get the **team** data included: "id": 1, "team": { "name": "Z-Force", - "headquarters": "Sister Margaret’s Bar", + "headquarters": "Sister Margaret's Bar", "id": 1 } } @@ -326,7 +246,7 @@ Now we get the list of **heroes** included: ## Recap -Using the same techniques to declare additonal **data models** we can tell FastAPI what data to return in the responses, even when we return **table models**. +Using the same techniques to declare additional **data models**, we can tell FastAPI what data to return in the responses, even when we return **table models**. Here we almost **didn't have to change the FastAPI app** code, but of course, there will be cases where you need to get the data and process it in different ways in the *path operation function* before returning it. @@ -334,4 +254,4 @@ But even in those cases, you will be able to define the **data models** to use i By this point, you already have a very robust API to handle data in a SQL database combining **SQLModel** with **FastAPI**, and implementing **best practices**, like data validation, conversion, filtering, and documentation. ✨ -In the next chapter I'll tell you how to implement automated **testing** for your application using FastAPI and SQLModel. ✅ +In the next chapter, I'll tell you how to implement automated **testing** for your application using FastAPI and SQLModel. ✅ diff --git a/docs/tutorial/fastapi/response-model.md b/docs/tutorial/fastapi/response-model.md index b4e0b6701e..f9214332c6 100644 --- a/docs/tutorial/fastapi/response-model.md +++ b/docs/tutorial/fastapi/response-model.md @@ -22,7 +22,7 @@ You can see that there's a possible "Successful Response" with a code `200`, but API docs UI without response data schemas -Right now we only tell FastAPI the data we want to receive, but we don't tell it yet the data we want to send back. +Right now, we only tell FastAPI the data we want to receive, but we don't tell it yet the data we want to send back. Let's do that now. 🤓 @@ -32,47 +32,15 @@ We can use `response_model` to tell FastAPI the schema of the data we want to se For example, we can pass the same `Hero` **SQLModel** class (because it is also a Pydantic model): -```Python hl_lines="3" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/response_model/tutorial001.py[ln:33-39]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/fastapi/response_model/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/fastapi/response_model/tutorial001_py310.py ln[31:37] hl[31] *} ## List of Heroes in `response_model` We can also use other type annotations, the same way we can use with Pydantic fields. For example, we can pass a list of `Hero`s. -First, we import `List` from `typing` and then we declare the `response_model` with `List[Hero]`: - -```Python hl_lines="1 5" -{!./docs_src/tutorial/fastapi/response_model/tutorial001.py[ln:1]!} - -# Code here omitted 👈 +To do so, we declare the `response_model` with `list[Hero]`: -{!./docs_src/tutorial/fastapi/response_model/tutorial001.py[ln:42-46]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/fastapi/response_model/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/fastapi/response_model/tutorial001_py310.py ln[40:44] hl[40] *} ## FastAPI and Response Model @@ -100,10 +68,13 @@ Additionally, because the schemas are defined in using a standard, there are man For example, client generators, that can automatically create the code necessary to talk to your API in many languages. -!!! info - If you are curious about the standards, FastAPI generates OpenAPI, that internally uses JSON Schema. +/// info + +If you are curious about the standards, FastAPI generates OpenAPI, that internally uses JSON Schema. + +You can read about all that in the FastAPI docs - First Steps. - You can read about all that in the FastAPI docs - First Steps. +/// ## Recap diff --git a/docs/tutorial/fastapi/session-with-dependency.md b/docs/tutorial/fastapi/session-with-dependency.md index 7f049f5002..e81e9e6745 100644 --- a/docs/tutorial/fastapi/session-with-dependency.md +++ b/docs/tutorial/fastapi/session-with-dependency.md @@ -6,22 +6,7 @@ Before we keep adding things, let's change a bit how we get the session for each Up to now, we have been creating a session in each *path operation*, in a `with` block. -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/delete/tutorial001.py[ln:50-57]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/fastapi/delete/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/fastapi/delete/tutorial001_py310.py ln[48:55] hl[50] *} That's perfectly fine, but in many use cases we would want to use FastAPI Dependencies, for example to **verify** that the client is **logged in** and get the **current user** before executing any other code in the *path operation*. @@ -35,22 +20,7 @@ A **FastAPI** dependency is very simple, it's just a function that returns a val It could use `yield` instead of `return`, and in that case **FastAPI** will make sure it executes all the code **after** the `yield`, once it is done with the request. -```Python hl_lines="3-5" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:42-44]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py ln[40:42] hl[40:42] *} ## Use the Dependency @@ -58,39 +28,21 @@ Now let's make FastAPI execute a dependency and get its value in the *path opera We import `Depends()` from `fastapi`. Then we use it in the *path operation function* in a **parameter**, the same way we declared parameters to get JSON bodies, path parameters, etc. -```Python hl_lines="3 15" -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:1-4]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:42-44]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:55-61]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview +{* ./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py ln[1:2,40:42,53:59] hl[1,54] *} -```Python -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py!} -``` +/// tip -
+Here's a tip about that `*,` thing in the parameters. -!!! tip - Here's a tip about that `*,` thing in the parameters. +Here we are passing the parameter `session` that has a "default value" of `Depends(get_session)` before the parameter `hero`, that doesn't have any default value. - Here we are passing the parameter `session` that has a "default value" of `Depends(get_session)` before the parameter `hero`, that doesn't have any default value. +Python would normally complain about that, but we can use the initial "parameter" `*,` to mark all the rest of the parameters as "keyword only", which solves the problem. - Python would normally complain about that, but we can use the initial "parameter" `*,` to mark all the rest of the parameters as "keyword only", which solves the problem. +You can read more about it in the FastAPI documentation Path Parameters and Numeric Validations - Order the parameters as you need, tricks - You can read more about it in the FastAPI documentation Path Parameters and Numeric Validations - Order the parameters as you need, tricks +/// -The value of a dependency will **only be used for one request**, FastAPI will call it right before calling your code, and will give you the value from that dependency. +The value of a dependency will **only be used for one request**, FastAPI will call it right before calling your code and will give you the value from that dependency. If it had `yield`, then it will continue the rest of the execution once you are done sending the response. In the case of the **session**, it will finish the cleanup code from the `with` block, closing the session, etc. @@ -104,55 +56,13 @@ And because dependencies can use `yield`, FastAPI will make sure to run the code This means that in the main code of the *path operation function*, it will work equivalently to the previous version with the explicit `with` block. -```Python hl_lines="16-20" -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:1-4]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:42-44]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:55-61]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py ln[1:2,40:42,53:59] hl[55:59] *} In fact, you could think that all that block of code inside of the `create_hero()` function is still inside a `with` block for the **session**, because this is more or less what's happening behind the scenes. But now, the `with` block is not explicitly in the function, but in the dependency above: -```Python hl_lines="9-10" -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:1-4]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:42-44]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:55-61]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py ln[1:2,40:42,53:59] hl[41:42] *} We will see how this is very useful when testing the code later. ✅ @@ -168,26 +78,7 @@ session: Session = Depends(get_session) And then we remove the previous `with` block with the old **session**. -```Python hl_lines="15 26 35 44 59" -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:1-4]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:42-44]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:55-107]!} -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py ln[1:2,40:42,53:104] hl[54,65,74,83,98] *} ## Recap diff --git a/docs/tutorial/fastapi/simple-hero-api.md b/docs/tutorial/fastapi/simple-hero-api.md index 8676136a46..79cf075e1b 100644 --- a/docs/tutorial/fastapi/simple-hero-api.md +++ b/docs/tutorial/fastapi/simple-hero-api.md @@ -8,22 +8,18 @@ The first step is to install FastAPI. FastAPI is the framework to create the **web API**. -But we also need another type of program to run it, it is called a "**server**". We will use **Uvicorn** for that. And we will install Uvicorn with its *standard* dependencies. - -Make sure you [have a virtual environment activated](../index.md#create-a-python-virtual-environment){.internal-link target=_blank}. - -Then install FastAPI and Uvicorn: +Make sure you create a [virtual environment](../../virtual-environments.md){.internal-link target=_blank}, activate it, and then install them, for example with:
```console -$ python -m pip install fastapi "uvicorn[standard]" +$ pip install fastapi "uvicorn[standard]" ---> 100% ```
-s + ## **SQLModel** Code - Models, Engine Now let's start with the SQLModel code. @@ -32,25 +28,7 @@ We will start with the **simplest version**, with just heroes (no teams yet). This is almost the same code we have seen up to now in previous examples: -```Python hl_lines="20-21" -{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py[ln:1]!} - -# One line of FastAPI imports here later 👈 -{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py[ln:4]!} - -{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py[ln:7-22]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py310.py ln[2,5:20] hl[19:20] *} There's only one change here from the code we have used before, the `check_same_thread` in the `connect_args`. @@ -62,10 +40,13 @@ But here we will make sure we don't share the same **session** in more than one And we also need to disable it because in **FastAPI** each request could be handled by multiple interacting threads. -!!! info - That's enough information for now, you can read more about it in the FastAPI docs for `async` and `await`. +/// info + +That's enough information for now, you can read more about it in the FastAPI docs for `async` and `await`. - The main point is, by ensuring you **don't share** the same **session** with more than one request, the code is already safe. +The main point is, by ensuring you **don't share** the same **session** with more than one request, the code is already safe. + +/// ## **FastAPI** App @@ -75,80 +56,39 @@ We will import the `FastAPI` class from `fastapi`. And then create an `app` object that is an instance of that `FastAPI` class: -```Python hl_lines="3 8" -{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py[ln:1-4]!} - -# SQLModel code here omitted 👈 - -{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py[ln:25]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py310.py ln[1:2,23] hl[1,23] *} ## Create Database and Tables on `startup` -We want to make sure that once the app starts running, the function `create_tables` is called. To create the database and tables. +We want to make sure that once the app starts running, the function `create_db_and_tables` is called. To create the database and tables. This should be called only once at startup, not before every request, so we put it in the function to handle the `"startup"` event: -```Python hl_lines="6-8" -# Code above omitted 👆 +{* ./docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py310.py ln[23:28] hl[26:28] *} -{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py[ln:25-30]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py!} -``` +## Create Heroes *Path Operation* -
+/// info -## Create Heroes *Path Operation* +If you need a refresher on what a **Path Operation** is (an endpoint with a specific HTTP Operation) and how to work with it in FastAPI, check out the FastAPI First Steps docs. -!!! info - If you need a refresher on what a **Path Operation** is (an endpoint with a specific HTTP Operation) and how to work with it in FastAPI, check out the FastAPI First Steps docs. +/// Let's create the **path operation** code to create a new hero. It will be called when a user sends a request with a `POST` **operation** to the `/heroes/` **path**: -```Python hl_lines="11-12" -# Code above omitted 👆 +{* ./docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py310.py ln[23:37] hl[31:32] *} -{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py[ln:25-39]!} +/// info -# Code below omitted 👇 -``` +If you need a refresher on some of those concepts, checkout the FastAPI documentation: -
-👀 Full file preview +* First Steps +* Path Parameters - Data Validation and Data Conversion +* Request Body -```Python -{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py!} -``` - -
- -!!! info - If you need a refresher on some of those concepts, checkout the FastAPI documentation: - - * First Steps - * Path Parameters - Data Validation and Data Conversion - * Request Body +/// ## The **SQLModel** Advantage @@ -158,31 +98,21 @@ Here we use the **same** class model to define the **request body** that will be Because **FastAPI** is based on Pydantic, it will use the same model (the Pydantic part) to do automatic data validation and conversion from the JSON request to an object that is an actual instance of the `Hero` class. -And then because this same **SQLModel** object is not only a **Pydantic** model instance but also a **SQLAlchemy** model instance, we can use it directly in a **session** to create the row in the database. +And then, because this same **SQLModel** object is not only a **Pydantic** model instance but also a **SQLAlchemy** model instance, we can use it directly in a **session** to create the row in the database. So we can use intuitive standard Python **type annotations**, and we don't have to duplicate a lot of the code for the database models and the API data models. 🎉 -!!! tip - We will improve this further later, but for now, it already shows the power of having **SQLModel** classes be both **SQLAlchemy** models and **Pydantic** models at the same time. - -## Read Heroes *Path Operation* - -Now let's add another **path operation** to read all the heroes: +/// tip -```Python hl_lines="20-24" -# Code above omitted 👆 +We will improve this further later, but for now, it already shows the power of having **SQLModel** classes be both **SQLAlchemy** models and **Pydantic** models at the same time. -{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py[ln:25-46]!} -``` +/// -
-👀 Full file preview +## Read Heroes *Path Operation* -```Python -{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py!} -``` +Now let's add another **path operation** to read all the heroes: -
+{* ./docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py310.py ln[23:44] hl[40:44] *} This is pretty straightforward. @@ -190,13 +120,13 @@ When a client sends a request to the **path** `/heroes/` with a `GET` HTTP **ope ## One Session per Request -Remember that we shoud use a SQLModel **session** per each group of operations and if we need other unrelated operations we should use a different session? +Remember that we should use a SQLModel **session** per each group of operations and if we need other unrelated operations we should use a different session? Here it is much more obvious. We should normally have **one session per request** in most of the cases. -In some isolated cases we would want to have new sessions inside, so, **more than one session** per request. +In some isolated cases, we would want to have new sessions inside, so, **more than one session** per request. But we would **never want to *share* the same session** among different requests. @@ -204,57 +134,48 @@ In this simple example, we just create the new sessions manually in the **path o In future examples later we will use a FastAPI Dependency to get the **session**, being able to share it with other dependencies and being able to replace it during testing. 🤓 -## Run the **FastAPI** Application +## Run the **FastAPI** Server in Development Mode Now we are ready to run the FastAPI application. Put all that code in a file called `main.py`. -Then run it with **Uvicorn**: +Then run it with the `fastapi` CLI, in development mode:
```console -$ uvicorn main:app +$ fastapi dev main.py INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [28720] -INFO: Started server process [28722] -INFO: Waiting for application startup. -INFO: Application startup complete. ```
-!!! info - The command `uvicorn main:app` refers to: +/// info + +The `fastapi` command uses Uvicorn underneath. + +/// - * `main`: the file `main.py` (the Python "module"). - * `app`: the object created inside of `main.py` with the line `app = FastAPI()`. +When you use `fastapi dev` it starts Uvicorn with the option to reload automatically every time you make a change to the code, this way you will be able to develop faster. 🤓 -### Uvicorn `--reload` +## Run the **FastAPI** Server in Production Mode -During development (and only during development), you can also add the option `--reload` to Uvicorn. +The development mode should not be used in production, as it includes automatic reload by default it consumes much more resources than necessary, and it would be more error prone, etc. -It will restart the server every time you make a change to the code, this way you will be able to develop faster. 🤓 +For production, use `fastapi run` instead of `fastapi dev`:
```console -$ uvicorn main:app --reload +$ fastapi run main.py -INFO: Will watch for changes in these directories: ['/home/user/code/sqlmodel-tutorial'] -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [28720] -INFO: Started server process [28722] -INFO: Waiting for application startup. -INFO: Application startup complete. +INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit) ```
-Just remember to never use `--reload` in production, as it consumes much more resources than necessary, would be more error prone, etc. - ## Check the API docs UI Now you can go to that URL in your browser `http://127.0.0.1:8000`. We didn't create a *path operation* for the root path `/`, so that URL alone will only show a "Not Found" error... that "Not Found" error is produced by your FastAPI application. @@ -275,9 +196,9 @@ And then you can get them back with the **Read Heroes** *path operation*: ## Check the Database -Now you can terminate that Uvicorn server by going back to the terminal and pressing Ctrl+C. +Now you can terminate that server program by going back to the terminal and pressing Ctrl+C. -And then you can open **DB Browser for SQLite** and check the database, to explore the data and confirm that it indeed saved the heroes. 🎉 +And then, you can open **DB Browser for SQLite** and check the database, to explore the data and confirm that it indeed saved the heroes. 🎉 DB Browser for SQLite showing the heroes @@ -287,4 +208,4 @@ Good job! This is already a FastAPI **web API** application to interact with the There are several things we can improve and extend. For example, we want the database to decide the ID of each new hero, we don't want to allow a user to send it. -We will do all those improvements in the next chapters. 🚀 +We will make all those improvements in the next chapters. 🚀 diff --git a/docs/tutorial/fastapi/teams.md b/docs/tutorial/fastapi/teams.md index 9bc4af78cf..4f07fb1981 100644 --- a/docs/tutorial/fastapi/teams.md +++ b/docs/tutorial/fastapi/teams.md @@ -12,26 +12,13 @@ Let's add the models for the teams. It's the same process we did for heroes, with a base model, a **table model**, and some other **data models**. -We have a `TeamBase` **data model**, and from it we inherit with a `Team` **table model**. +We have a `TeamBase` **data model**, and from it, we inherit with a `Team` **table model**. -Then we also inherit from the `TeamBase` for the `TeamCreate` and `TeamRead` **data models**. +Then we also inherit from the `TeamBase` for the `TeamCreate` and `TeamPublic` **data models**. And we also create a `TeamUpdate` **data model**. -```Python hl_lines="7-9 12-15 18-19 22-23 26-29" -{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:1-29]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/fastapi/teams/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/fastapi/teams/tutorial001_py310.py ln[1:26] hl[5:7,10:13,16:17,20:21,24:26] *} We now also have **relationship attributes**. 🎉 @@ -39,22 +26,7 @@ Let's now update the `Hero` models too. ## Update Hero Models -```Python hl_lines="3-8 11-15 17-18 21-22 25-29" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:32-58]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/fastapi/teams/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/fastapi/teams/tutorial001_py310.py ln[29:55] hl[29:34,37:40,43:44,47:48,51:55] *} We now have a `team_id` in the hero models. @@ -66,22 +38,7 @@ And even though the `HeroBase` is *not* a **table model**, we can declare `team_ Notice that the **relationship attributes**, the ones with `Relationship()`, are **only** in the **table models**, as those are the ones that are handled by **SQLModel** with SQLAlchemy and that can have the automatic fetching of data from the database when we access them. -```Python hl_lines="11 39" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:7-58]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/fastapi/teams/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/fastapi/teams/tutorial001_py310.py ln[5:55] hl[13,40] *} ## Path Operations for Teams @@ -89,28 +46,13 @@ Let's now add the **path operations** for teams. These are equivalent and very similar to the **path operations** for the **heroes** we had before, so we don't have to go over the details for each one, let's check the code. -```Python hl_lines="3-9 12-20 23-28 31-47 50-57" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:140-194]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/fastapi/teams/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/fastapi/teams/tutorial001_py310.py ln[135:188] hl[135:141,144:152,155:160,163:178,181:188] *} ## Using Relationships Attributes -Up to this point we are actually not using the **relationship attributes**, but we could access them in our code. +Up to this point, we are actually not using the **relationship attributes**, but we could access them in our code. -In the next chapter we will play more with them. +In the next chapter, we will play more with them. ## Check the Docs UI diff --git a/docs/tutorial/fastapi/tests.md b/docs/tutorial/fastapi/tests.md index eaf3ef380f..f7fd92c9cb 100644 --- a/docs/tutorial/fastapi/tests.md +++ b/docs/tutorial/fastapi/tests.md @@ -14,14 +14,7 @@ We will use the application with the hero models, but without team models, and w Now we will see how useful it is to have this session dependency. ✨ -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/fastapi/app_testing/tutorial001/main.py!} -``` - -
+{* ./docs_src/tutorial/fastapi/app_testing/tutorial001_py310/main.py ln[0] *} ## File Structure @@ -43,12 +36,12 @@ If you haven't done testing in FastAPI applications, first check the ```console -$ python -m pip install requests pytest +$ pip install requests pytest ---> 100% ``` @@ -60,29 +53,32 @@ $ python -m pip install requests pytest Let's start with a simple test, with just the basic test code we need the check that the **FastAPI** application is creating a new hero correctly. ```{ .python .annotate } -{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_001.py[ln:1-7]!} +{!./docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_001.py[ln:1-7]!} # Some code here omitted, we will see it later 👈 -{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_001.py[ln:20-24]!} +{!./docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_001.py[ln:20-24]!} # Some code here omitted, we will see it later 👈 -{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_001.py[ln:26-32]!} +{!./docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_001.py[ln:26-32]!} # Code below omitted 👇 ``` -{!./docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_001.md!} +{!./docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_001.md!} + +/// tip + +Check out the number bubbles to see what is done by each line of code. -!!! tip - Check out the number bubbles to see what is done by each line of code. +/// That's the **core** of the code we need for all the tests later. -But now we need to deal with a bit of logistics and details we are not paying attention to just yet. 🤓 +But now, we need to deal with a bit of logistics and details we are not paying attention to just yet. 🤓 ## Testing Database This test looks fine, but there's a problem. -If we run it, it will use the same **production database** that we are using to store our very important **heroes**, and we will end up adding adding unnecesary data to it, or even worse, in future tests we could end up removing production data. +If we run it, it will use the same **production database** that we are using to store our very important **heroes**, and we will end up adding unnecessary data to it, or even worse, in future tests we could end up removing production data. So, we should use an independent **testing database**, just for the tests. @@ -107,17 +103,20 @@ We will override it to use a different **session** object just for the tests. That way we protect the production database and we have better control of the data we are testing. ```{ .python .annotate hl_lines="4 9-10 12 19" } -{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_002.py[ln:1-7]!} +{!./docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_002.py[ln:1-7]!} # Some code here omitted, we will see it later 👈 -{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_002.py[ln:15-32]!} +{!./docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_002.py[ln:15-32]!} # Code below omitted 👇 ``` -{!./docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_002.md!} +{!./docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_002.md!} -!!! tip - Check out the number bubbles to see what is done by each line of code. +/// tip + +Check out the number bubbles to see what is done by each line of code. + +/// ## Create the Engine and Session for Testing @@ -132,10 +131,10 @@ sqlite:///testing.db So, the testing database will be in the file `testing.db`. ``` { .python .annotate hl_lines="4 8-11 13 16 33"} -{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_003.py!} +{!./docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_003.py!} ``` -{!./docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_003.md!} +{!./docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_003.md!} ### Import Table Models @@ -155,7 +154,7 @@ That way, when we call `.create_all()` all the **table models** are correctly re ## Memory Database -Now we are not using the production database, instead we use a **new testing database** with the `testing.db` file, which is great. +Now we are not using the production database. Instead, we use a **new testing database** with the `testing.db` file, which is great. But SQLite also supports having an **in memory** database. This means that all the database is only in memory, and it is never saved in a file on disk. @@ -165,13 +164,11 @@ But **it works great for testing**, because it can be quickly created before eac And also, because it never has to write anything to a file and it's all just in memory, it will be even faster than normally. 🏎 -
- -Other alternatives and ideas 👀 - +/// details | Other alternatives and ideas 👀 + Before arriving at the idea of using an **in-memory database** we could have explored other alternatives and ideas. -The first, is that we are not deleting the file after we finish the test, so, the next test could have **leftover data**. So, the right thing would be to delete the file right after finishing the test. 🔥 +The first is that we are not deleting the file after we finish the test, so the next test could have **leftover data**. So, the right thing would be to delete the file right after finishing the test. 🔥 But if each test has to create a new file and then delete it afterwards, running all the tests could be **a bit slow**. @@ -179,9 +176,9 @@ Right now, we have a file `testing.db` that is used by all the tests (we only ha So, if we tried to run the tests at the same time **in parallel** to try to speed things up a bit, they would clash trying to use the *same* `testing.db` file. -Of couse, we could also fix that, using some **random name** for each testing database file... but in the case of SQLite, we have an even better alternative with just using an **in-memory database**. ✨ +Of course, we could also fix that, using some **random name** for each testing database file... but in the case of SQLite, we have an even better alternative by just using an **in-memory database**. ✨ -
+/// ## Configure the In-Memory Database @@ -190,15 +187,18 @@ Let's update our code to use the in-memory database. We just have to change a couple of parameters in the **engine**. ```{ .python .annotate hl_lines="3 9-13"} -{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_004.py[ln:1-13]!} +{!./docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_004.py[ln:1-13]!} # Code below omitted 👇 ``` -{!./docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_004.md!} +{!./docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_004.md!} + +/// tip + +Check out the number bubbles to see what is done by each line of code. -!!! tip - Check out the number bubbles to see what is done by each line of code. +/// That's it, now the test will run using the **in-memory database**, which will be faster and probably safer. @@ -208,14 +208,17 @@ And all the other tests can do the same. Great, that works, and you could replicate all that process in each of the test functions. -But we had to add a lot of **boilerplate code** to handle the custom database, creating it in memory, the custom session, the dependency override. +But we had to add a lot of **boilerplate code** to handle the custom database, creating it in memory, the custom session, and the dependency override. Do we really have to duplicate all that for **each test**? No, we can do better! 😎 We are using **pytest** to run the tests. And pytest also has a very similar concept to the **dependencies in FastAPI**. -!!! info - In fact, pytest was one of the things that inspired the design of the dependencies in FastAPI. +/// info + +In fact, pytest was one of the things that inspired the design of the dependencies in FastAPI. + +/// It's a way for us to declare some **code that should be run before** each test and **provide a value** for the test function (that's pretty much the same as FastAPI dependencies). @@ -232,22 +235,25 @@ You can read more about them in the
-👀 Full file preview +That's why we add these two extra tests here. -```Python -{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main.py!} -``` +/// - - -!!! tip - It's always **good idea** to not only test the normal case, but also that **invalid data**, **errors**, and **corner cases** are handled correctly. - - That's why we add these two extra tests here. - -Now, any additional test functions can be as **simple** as the first one, they just have to **declate the `client` parameter** to get the `TestClient` **fixture** with all the database stuff setup. Nice! 😎 +Now, any additional test functions can be as **simple** as the first one, they just have to **declare the `client` parameter** to get the `TestClient` **fixture** with all the database stuff setup. Nice! 😎 ## Why Two Fixtures -Now, seeing the code we could think, why do we put **two fixtures** instead of **just one** with all the code? And that makes total sense! +Now, seeing the code, we could think, why do we put **two fixtures** instead of **just one** with all the code? And that makes total sense! -For these examples, **that would have been simpler**, there's no need to separate that code in two fixtures for them... +For these examples, **that would have been simpler**, there's no need to separate that code into two fixtures for them... But for the next test function, we will require **both fixtures**, the **client** and the **session**. -```Python hl_lines="6 10" -{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main.py[ln:1-6]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main.py[ln:61-81]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main.py!} -``` - -
+{* ./docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main.py ln[1:6,61:81] hl[6,61] *} -In this test function we want to check that the *path operation* to **read a list of heroes** actually sends us heroes. +In this test function, we want to check that the *path operation* to **read a list of heroes** actually sends us heroes. But if the **database is empty**, we would get an **empty list**, and we wouldn't know if the hero data is being sent correctly or not. @@ -362,22 +342,9 @@ The function for the **client fixture** and the actual testing function will **b ## Add the Rest of the Tests -Using the same ideas, requiring the fixtures, creating data that we need for the tests, etc. we can now add the rest of the tests, they look quite similar to what we have done up to now. - -```Python hl_lines="3 18 33" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main.py[ln:84-125]!} -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main.py!} -``` +Using the same ideas, requiring the fixtures, creating data that we need for the tests, etc., we can now add the rest of the tests. They look quite similar to what we have done up to now. -
+{* ./docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main.py ln[84:125] hl[84,99,114] *} ## Run the Tests @@ -389,7 +356,7 @@ Now we can run the tests with `pytest` and see the results: $ pytest ============= test session starts ============== -platform linux -- Python 3.7.5, pytest-6.2.4, py-1.10.0, pluggy-0.13.1 +platform linux -- Python 3.10.0, pytest-7.4.4, pluggy-1.5.0 rootdir: /home/user/code/sqlmodel-tutorial collected 7 items @@ -406,9 +373,9 @@ project/test_main.py ....... [100%] Did you read all that? Wow, I'm impressed! 😎 -Adding tests to your application will give you a lot of **certainty** that everything is **working correctly**, as you indended. +Adding tests to your application will give you a lot of **certainty** that everything is **working correctly**, as you intended. -And tests will be notoriously useful when **refactoring** your code, **changing things**, **adding features**. Because tests they can help catch a lot of errors that can be easily introduced by refactoring. +And tests will be notoriously useful when **refactoring** your code, **changing things**, **adding features**. Because tests can help catch a lot of errors that can be easily introduced by refactoring. And they will give you the confidence to work faster and **more efficiently**, because you know that you are checking if you are **not breaking anything**. 😅 diff --git a/docs/tutorial/fastapi/update-extra-data.md b/docs/tutorial/fastapi/update-extra-data.md new file mode 100644 index 0000000000..95843144ae --- /dev/null +++ b/docs/tutorial/fastapi/update-extra-data.md @@ -0,0 +1,143 @@ +# Update with Extra Data (Hashed Passwords) with FastAPI + +In the previous chapter I explained to you how to update data in the database from input data coming from a **FastAPI** *path operation*. + +Now I'll explain to you how to add **extra data**, additional to the input data, when updating or creating a model object. + +This is particularly useful when you need to **generate some data** in your code that is **not coming from the client**, but you need to store it in the database. For example, to store a **hashed password**. + +## Password Hashing + +Let's imagine that each hero in our system also has a **password**. + +We should never store the password in plain text in the database, we should only stored a **hashed version** of it. + +"**Hashing**" means converting some content (a password in this case) into a sequence of bytes (just a string) that looks like gibberish. + +Whenever you pass exactly the same content (exactly the same password) you get exactly the same gibberish. + +But you **cannot convert** from the gibberish **back to the password**. + +### Why use Password Hashing + +If your database is stolen, the thief won't have your users' **plaintext passwords**, only the hashes. + +So, the thief won't be able to try to use that password in another system (as many users use the same password everywhere, this would be dangerous). + +/// tip + +You could use
passlib to hash passwords. + +In this example we will use a fake hashing function to focus on the data changes. 🤡 + +/// + +## Update Models with Extra Data + +The `Hero` table model will now store a new field `hashed_password`. + +And the data models for `HeroCreate` and `HeroUpdate` will also have a new field `password` that will contain the plain text password sent by clients. + +{* ./docs_src/tutorial/fastapi/update/tutorial002_py310.py ln[5:28] hl[13,17,28] *} + +When a client is creating a new hero, they will send the `password` in the request body. + +And when they are updating a hero, they could also send the `password` in the request body to update it. + +## Hash the Password + +The app will receive the data from the client using the `HeroCreate` model. + +This contains the `password` field with the plain text password, and we cannot use that one. So we need to generate a hash from it. + +{* ./docs_src/tutorial/fastapi/update/tutorial002_py310.py ln[42:44,55:57] hl[57] *} + +## Create an Object with Extra Data + +Now we need to create the database hero. + +In previous examples, we have used something like: + +```Python +db_hero = Hero.model_validate(hero) +``` + +This creates a `Hero` (which is a *table model*) object from the `HeroCreate` (which is a *data model*) object that we received in the request. + +And this is all good... but as `Hero` doesn't have a field `password`, it won't be extracted from the object `HeroCreate` that has it. + +`Hero` actually has a `hashed_password`, but we are not providing it. We need a way to provide it... + +### Dictionary Update + +Let's pause for a second to check this, when working with dictionaries, there's a way to `update` a dictionary with extra data from another dictionary, something like this: + +```Python hl_lines="14" +db_user_dict = { + "name": "Deadpond", + "secret_name": "Dive Wilson", + "age": None, +} + +hashed_password = "fakehashedpassword" + +extra_data = { + "hashed_password": hashed_password, + "age": 32, +} + +db_user_dict.update(extra_data) + +print(db_user_dict) + +# { +# "name": "Deadpond", +# "secret_name": "Dive Wilson", +# "age": 32, +# "hashed_password": "fakehashedpassword", +# } +``` + +This `update` method allows us to add and override things in the original dictionary with the data from another dictionary. + +So now, `db_user_dict` has the updated `age` field with `32` instead of `None` and more importantly, **it has the new `hashed_password` field**. + +### Create a Model Object with Extra Data + +Similar to how dictionaries have an `update` method, **SQLModel** models have a parameter `update` in `Hero.model_validate()` that takes a dictionary with extra data, or data that should take precedence: + +{* ./docs_src/tutorial/fastapi/update/tutorial002_py310.py ln[55:64] hl[60] *} + +Now, `db_hero` (which is a *table model* `Hero`) will extract its values from `hero` (which is a *data model* `HeroCreate`), and then it will **`update`** its values with the extra data from the dictionary `extra_data`. + +It will only take the fields defined in `Hero`, so **it will not take the `password`** from `HeroCreate`. And it will also **take its values** from the **dictionary passed to the `update`** parameter, in this case, the `hashed_password`. + +If there's a field in both `hero` and the `extra_data`, **the value from the `extra_data` passed to `update` will take precedence**. + +## Update with Extra Data + +Now let's say we want to **update a hero** that already exists in the database. + +The same way as before, to avoid removing existing data, we will use `exclude_unset=True` when calling `hero.model_dump()`, to get a dictionary with only the data sent by the client. + +{* ./docs_src/tutorial/fastapi/update/tutorial002_py310.py ln[83:89] hl[89] *} + +Now, this `hero_data` dictionary could contain a `password`. We need to check it, and if it's there, we need to generate the `hashed_password`. + +Then we can put that `hashed_password` in a dictionary. + +And then we can update the `db_hero` object using the method `db_hero.sqlmodel_update()`. + +It takes a model object or dictionary with the data to update the object and also an **additional `update` argument** with extra data. + +{* ./docs_src/tutorial/fastapi/update/tutorial002_py310.py ln[83:99] hl[95] *} + +/// tip + +The method `db_hero.sqlmodel_update()` was added in SQLModel 0.0.16. 😎 + +/// + +## Recap + +You can use the `update` parameter in `Hero.model_validate()` to provide extra data when creating a new object and `Hero.sqlmodel_update()` to provide extra data when updating an existing object. 🤓 diff --git a/docs/tutorial/fastapi/update.md b/docs/tutorial/fastapi/update.md index b32e58281d..e3c8ac6ac7 100644 --- a/docs/tutorial/fastapi/update.md +++ b/docs/tutorial/fastapi/update.md @@ -4,37 +4,25 @@ Now let's see how to update data in the database with a **FastAPI** *path operat ## `HeroUpdate` Model -We want clients to be able to udpate the `name`, the `secret_name`, and the `age` of a hero. +We want clients to be able to update the `name`, the `secret_name`, and the `age` of a hero. But we don't want them to have to include all the data again just to **update a single field**. -So, we need to have all those fields **marked as optional**. +So, we need to make all those fields **optional**. -And because the `HeroBase` has some of them as *required* and not optional, we will need to **create a new model**. +And because the `HeroBase` has some of them *required* (without a default value), we will need to **create a new model**. -!!! tip - Here is one of those cases where it probably makes sense to use an **independent model** instead of trying to come up with a complex tree of models inheriting from each other. +/// tip - Because each field is **actually different** (we just change it to `Optional`, but that's already making it different), it makes sense to have them in their own model. +Here is one of those cases where it probably makes sense to use an **independent model** instead of trying to come up with a complex tree of models inheriting from each other. -So, let's create this new `HeroUpdate` model: - -```Python hl_lines="21-24" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/update/tutorial001.py[ln:7-28]!} +Because each field is **actually different** (we just set a default value of `None`, but that's already making it different), it makes sense to have them in their own model. -# Code below omitted 👇 -``` - -
-👀 Full file preview +/// -```Python -{!./docs_src/tutorial/fastapi/update/tutorial001.py!} -``` +So, let's create this new `HeroUpdate` model: -
+{* ./docs_src/tutorial/fastapi/update/tutorial001_py310.py ln[5:26] hl[23:26] *} This is almost the same as `HeroBase`, but all the fields are optional, so we can't simply inherit from `HeroBase`. @@ -44,24 +32,9 @@ Now let's use this model in the *path operation* to update a hero. We will use a `PATCH` HTTP operation. This is used to **partially update data**, which is what we are doing. -```Python hl_lines="3-4" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/update/tutorial001.py[ln:76-91]!} - -# Code below omitted 👇 -``` +{* ./docs_src/tutorial/fastapi/update/tutorial001_py310.py ln[74:89] hl[74:75] *} -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/fastapi/update/tutorial001.py!} -``` - -
- -We also read the `hero_id` from the *path parameter* an the request body, a `HeroUpdate`. +We also read the `hero_id` from the *path parameter* and the request body, a `HeroUpdate`. ### Read the Existing Hero @@ -69,28 +42,13 @@ We take a `hero_id` with the **ID** of the hero **we want to update**. So, we need to read the hero from the database, with the **same logic** we used to **read a single hero**, checking if it exists, possibly raising an error for the client if it doesn't exist, etc. -```Python hl_lines="6-8" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/update/tutorial001.py[ln:76-91]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/fastapi/update/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/fastapi/update/tutorial001_py310.py ln[74:89] hl[77:79] *} ### Get the New Data The `HeroUpdate` model has all the fields with **default values**, because they all have defaults, they are all optional, which is what we want. -But that also means that if we just call `hero.dict()` we will get a dictionary that could potentially have several or all of those values with their defaults, for example: +But that also means that if we just call `hero.model_dump()` we will get a dictionary that could potentially have several or all of those values with their defaults, for example: ```Python { @@ -100,9 +58,9 @@ But that also means that if we just call `hero.dict()` we will get a dictionary } ``` -And then if we update the hero in the database with this data, we would be removing any existing values, and that's probably **not what the client intended**. +And then, if we update the hero in the database with this data, we would be removing any existing values, and that's probably **not what the client intended**. -But fortunately Pydantic models (and so SQLModel models) have a parameter we can pass to the `.dict()` method for that: `exclude_unset=True`. +But fortunately Pydantic models (and so SQLModel models) have a parameter we can pass to the `.model_dump()` method for that: `exclude_unset=True`. This tells Pydantic to **not include** the values that were **not sent** by the client. Saying it another way, it would **only** include the values that were **sent by the client**. @@ -112,7 +70,7 @@ So, if the client sent a JSON with no values: {} ``` -Then the dictionary we would get in Python using `hero.dict(exclude_unset=True)` would be: +Then the dictionary we would get in Python using `hero.model_dump(exclude_unset=True)` would be: ```Python {} @@ -126,7 +84,7 @@ But if the client sent a JSON with: } ``` -Then the dictionary we would get in Python using `hero.dict(exclude_unset=True)` would be: +Then the dictionary we would get in Python using `hero.model_dump(exclude_unset=True)` would be: ```Python { @@ -136,57 +94,29 @@ Then the dictionary we would get in Python using `hero.dict(exclude_unset=True)` Then we use that to get the data that was actually sent by the client: -```Python hl_lines="9" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/update/tutorial001.py[ln:76-91]!} - -# Code below omitted 👇 -``` +{* ./docs_src/tutorial/fastapi/update/tutorial001_py310.py ln[74:89] hl[80] *} -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/fastapi/update/tutorial001.py!} -``` - -
+/// tip +Before SQLModel 0.0.14, the method was called `hero.dict(exclude_unset=True)`, but it was renamed to `hero.model_dump(exclude_unset=True)` to be consistent with Pydantic v2. +/// ## Update the Hero in the Database -Now that we have a **dictionary with the data sent by the client**, we can iterate for each one of the keys and the values, and then we set them in the database hero model `db_hero` using `setattr()`. +Now that we have a **dictionary with the data sent by the client**, we can use the method `db_hero.sqlmodel_update()` to update the object `db_hero`. -```Python hl_lines="10-11" -# Code above omitted 👆 +{* ./docs_src/tutorial/fastapi/update/tutorial001_py310.py ln[74:89] hl[81] *} -{!./docs_src/tutorial/fastapi/update/tutorial001.py[ln:76-91]!} +/// tip -# Code below omitted 👇 -``` +The method `db_hero.sqlmodel_update()` was added in SQLModel 0.0.16. 🤓 -
-👀 Full file preview +Before that, you would need to manually get the values and set them using `setattr()`. -```Python -{!./docs_src/tutorial/fastapi/update/tutorial001.py!} -``` - -
- -If you are not familiar with that `setattr()`, it takes an object, like the `db_hero`, then an attribute name (`key`), that in our case could be `"name"`, and a value (`value`). And then it **sets the attribute with that name to the value**. - -So, if `key` was `"name"` and `value` was `"Deadpuddle"`, then this code: +/// -```Python -setattr(db_hero, key, value) -``` - -...would be more or less equivalent to: +The method `db_hero.sqlmodel_update()` takes an argument with another model object or a dictionary. -```Python -db_hero.name = "Deadpuddle" -``` +For each of the fields in the **original** model object (`db_hero` in this example), it checks if the field is available in the **argument** (`hero_data` in this example) and then updates it with the provided value. ## Remove Fields @@ -200,7 +130,7 @@ We are **not simply omitting** the data that has the **default values**. And we are **not simply omitting** anything that is `None`. -This means that, if a model in the database **has a value different than the default**, the client could **reset it to the same value as the default**, or even `None`, and we would **still notice it** and **update it accordingly**. 🤯🚀 +This means that if a model in the database **has a value different than the default**, the client could **reset it to the same value as the default**, or even `None`, and we would **still notice it** and **update it accordingly**. 🤯🚀 So, if the client wanted to intentionally remove the `age` of a hero, they could just send a JSON with: @@ -210,7 +140,7 @@ So, if the client wanted to intentionally remove the `age` of a hero, they could } ``` -And when getting the data with `hero.dict(exclude_unset=True)`, we would get: +And when getting the data with `hero.model_dump(exclude_unset=True)`, we would get: ```Python { @@ -218,14 +148,14 @@ And when getting the data with `hero.dict(exclude_unset=True)`, we would get: } ``` -So, we would use that value and upate the `age` to `None` in the database, **just as the client intended**. +So, we would use that value and update the `age` to `None` in the database, **just as the client intended**. Notice that `age` here is `None`, and **we still detected it**. -Also that `name` was not even sent, and we don't *accidentaly* set it to `None` or something, we just didn't touch it, because the client didn't sent it, so we are **pefectly fine**, even in these corner cases. ✨ +Also, that `name` was not even sent, and we don't *accidentally* set it to `None` or something. We just didn't touch it because the client didn't send it, so we are **perfectly fine**, even in these corner cases. ✨ These are some of the advantages of Pydantic, that we can use with SQLModel. 🎉 ## Recap -Using `.dict(exclude_unset=True)` in SQLModel models (and Pydantic models) we can easily update data **correctly**, even in the **edge cases**. 😎 +Using `.model_dump(exclude_unset=True)` in SQLModel models (and Pydantic models) we can easily update data **correctly**, even in the **edge cases**. 😎 diff --git a/docs/tutorial/index.md b/docs/tutorial/index.md index b45881138d..dd61e8597e 100644 --- a/docs/tutorial/index.md +++ b/docs/tutorial/index.md @@ -1,12 +1,14 @@ -# Intro, Installation, and First Steps +# Tutorial - User Guide + +In this tutorial you will learn how to use **SQLModel**. ## Type hints -If you need a refreshed about how to use Python type hints (type annotations), check FastAPI's Python types intro. +If you need a refresher about how to use Python type hints (type annotations), check FastAPI's Python types intro. You can also check the mypy cheat sheet. -**SQLModel** uses type annotations for everything, this way you can use a familiar Python syntax and get all the editor support posible, with autocompletion and in-editor error checking. +**SQLModel** uses type annotations for everything, this way you can use a familiar Python syntax and get all the editor support possible, with autocompletion and in-editor error checking. ## Intro @@ -29,191 +31,3 @@ Using it in your editor is what really shows you the benefits of **SQLModel**, s Running the examples is what will really help you understand what is going on. You can learn a lot more by running some examples and playing around with them than by reading all the docs here. - -## Create a Project - -Please go ahead and create a directory for the project we will work on on this tutorial. - -What I normally do is that I create a directory named `code` inside my home/user directory. - -And inside of that I create one directory per project. - -So, for example: - -
- -```console -// Go to the home directory -$ cd -// Create a directory for all your code projects -$ mkdir code -// Enter into that code directory -$ cd code -// Create a directory for this project -$ mkdir sqlmodel-tutorial -// Enter into that directory -$ cd sqlmodel-tutorial -``` - -
- -!!! tip - Make sure you don't name it also `sqlmodel`, so that you don't end up overriding the name of the package. - -### Make sure you have Python - -Make sure you have an officially supported version of Python. - -Currently it is **Python 3.6** and above (Python 3.5 was already deprecated). - -You can check which version you have with: - -
- -```console -$ python3 --version -Python 3.6.9 -``` - -
- -There's a chance that you have multiple Python versions installed. - -You might want to try with the specific versions, for example with: - -* `python3.10` -* `python3.9` -* `python3.8` -* `python3.7` -* `python3.6` - -The code would look like this: - -
- -```console -// Let's check with just python3 -$ python3 --version -// This is too old! 😱 -Python 3.5.6 -// Let's see if python3.10 is available -$ python3.10 --version -// Oh, no, this one is not available 😔 -command not found: python3.10 -$ python3.9 --version -// Nice! This works 🎉 -Python 3.9.0 -// In this case, we would continue using python3.9 instead of python3 -``` - -
- -If you have different versions and `python3` is not the latest, make sure you use the latest version you have available. For example `python3.9`. - -If you don't have a valid Python version installed, go and install that first. - -### Create a Python virtual environment - -When writing Python code, you should **always** use virtual environments in one way or another. - -If you don't know what that is, you can read the official tutorial for virtual environments, it's quite simple. - -In very short, a virtual environment is a small directory that contains a copy of Python and all the libraries you need to run your code. - -And when you "activate" it, any package that you install, for example with `pip`, will be installed in that virtual environment. - -!!! tip - There are other tools to manage virtual environments, like Poetry. - - And there are alternatives that are particularly useful for deployment like Docker and other types of containers. In this case, the "virtual environment" is not just the Python standard files and the installed packages, but the whole system. - -Go ahead and create a Python virtual environment for this project. And make sure to also upgrade `pip`. - -Here are the commands you could use: - -=== "Linux, macOS, Linux in Windows" - -
- - ```console - // Remember that you might need to use python3.9 or similar 💡 - // Create the virtual environment using the module "venv" - $ python3 -m venv env - // ...here it creates the virtual enviroment in the directory "env" - // Activate the virtual environment - $ source ./env/bin/activate - // Verify that the virtual environment is active - # (env) $$ which python - // The important part is that it is inside the project directory, at "code/sqlmodel-tutorial/env/bin/python" - /home/leela/code/sqlmodel-tutorial/env/bin/python - // Use the module "pip" to install and upgrade the package "pip" 🤯 - # (env) $$ python -m pip install --upgrade pip - ---> 100% - Successfully installed pip - ``` - -
- -=== "Windows PowerShell" - -
- - ```console - // Create the virtual environment using the module "venv" - # >$ python3 -m venv env - // ...here it creates the virtual enviroment in the directory "env" - // Activate the virtual environment - # >$ .\env\Scripts\Activate.ps1 - // Verify that the virtual environment is active - # (env) >$ Get-Command python - // The important part is that it is inside the project directory, at "code\sqlmodel-tutorial\env\python.exe" - CommandType Name Version Source - ----------- ---- ------- ------ - Application python 0.0.0.0 C:\Users\leela\code\sqlmodel-tutorial\env\python.exe - // Use the module "pip" to install and upgrade the package "pip" 🤯 - # (env) >$ python3 -m pip install --upgrade pip - ---> 100% - Successfully installed pip - ``` - -
- -## Install **SQLModel** - -Now, after making sure we are inside of a virtual environment in some way, we can install **SQLModel**: - -
- -```console -# (env) $$ python -m pip install sqlmodel ----> 100% -Successfully installed sqlmodel pydantic sqlalchemy -``` - -
- -As **SQLModel** is built on top of SQLAlchemy and Pydantic, when you install `sqlmodel` they will also be automatically installed. - -## Install DB Browser for SQLite - -Remember that [SQLite is a simple database in a single file](../databases.md#a-single-file-database){.internal-link target=_blank}? - -For most of the tutorial I'll use SQLite for the examples. - -Python has integrated support for SQLite, it is a single file read and processed from Python. And it doesn't need an [External Database Server](../databases.md#a-server-database){.internal-link target=_blank}, so it will be perfect for learning. - -In fact, SQLite is perfectly capable of handling quite big applications. At some point you might want to migrate to a server-based database like PostgreSQL (which is also free). But for now we'll stick to SQLite. - -Through the tutorial I will show you SQL fragments, and Python examples. And I hope (and expect 🧐) you to actually run them, and verify that the database is working as expected and showing you the same data. - -To be able to explore the SQLite file yourself, independent of Python code (and probably at the same time), I recommend you use DB Browser for SQLite. - -It's a great and simple program to interact with SQLite databases (SQLite files) in a nice user interface. - - - -Go ahead and Install DB Browser for SQLite, it's free. - -## Next Steps - -Okay, let's get going! On the [next section](create-db-and-table-with-db-browser.md) we'll start creating a database. 🚀 diff --git a/docs/tutorial/indexes.md b/docs/tutorial/indexes.md index 6513d7d462..459ee8ce34 100644 --- a/docs/tutorial/indexes.md +++ b/docs/tutorial/indexes.md @@ -20,14 +20,7 @@ Are you already a **SQL expert** and don't have time for all my explanations? Fine, in that case, you can **sneak peek** the final code to create indexes here. -
-👀 Full file preview - -```Python hl_lines="8 10" -{!./docs_src/tutorial/indexes/tutorial002.py!} -``` - -
+{* ./docs_src/tutorial/indexes/tutorial002_py310.py ln[0] *} ..but if you are not an expert, **continue reading**, this will probably be useful. 🤓 @@ -41,44 +34,47 @@ Imagine a **dictionary**, a book with definitions of words. 📔 ...not a Python Let's say that you want to **find a word**, for example the word "**database**". You take the dictionary, and open it somewhere, for example in the middle. Maybe you see some definitions of words that start with `m`, like `manual`, so you conclude that you are in the letter `m` in the dictionary. - + You know that in the alphabet, the letter `d` for `database` comes **before** the letter `m` for `manual`. - + So, you know you have to search in the dictionary **before** the point you currently are. You still don't know where the word `database` is, because you don't know exactly where the letter `d` is in the dictionary, but you know that **it is not after** that point, you can now **discard the right half** of the dictionary in your search. - + Next, you **open the dictionary again**, but only taking into account the **half of the dictionary** that can contain the word you want, the **left part of the dictionary**. You open it in the middle of that left part and now you arrive maybe at the letter `f`. - + You know that `d` from `database` comes before `f`. So it has to be **before** that. But now you know that `database` **is not after** that point, and you can discard the dictionary from that point onward. - + Now you have a **small section of dictionary** to search (only a **quarter** of dictionary can have your word). You take that **quarter** of the pages at the start of the dictionary that can contain your word, and open it in the middle of that section. Maybe you arrive at the letter `c`. - + You know the word `database` has to be **after** that and **not before** that point, so you can discard the left part of that block of pages. - + You repeat this process **a few more times**, and you finally arrive at the letter `d`, you continue with the same process in that section for the letter `d` and you finally **find the word** `database`. 🎉 - + You had to open the dictionary a few times, maybe **5 or 10**. That's actually **very little work** compared to what it could have been. -!!! note "Technical Details" - Do you like **fancy words**? Cool! Programmers tend to like fancy words. 😅 +/// note | Technical Details + +Do you like **fancy words**? Cool! Programmers tend to like fancy words. 😅 - That algorithm I showed you above is called **Binary Search**. +That algorithm I showed you above is called **Binary Search**. - It's called like that because you **search** something by splitting the dictionary (or any ordered list of things) in **two** ("binary" means "two") parts. And you do that process multiple times until you find what you want. +It's called that because you **search** something by splitting the dictionary (or any ordered list of things) in **two** ("binary" means "two") parts. And you do that process multiple times until you find what you want. + +/// ### An Index and a Novel @@ -100,7 +96,7 @@ Open the index, and after **5 or 10 steps**, quickly find the topic "**database* Now you know that you need to find "**page 253**". But by looking at the closed book you still don't know where that page is, so you have to **find that page**. To find it, you can do the same process again, but this time, instead of searching for a **topic** in the **index**, you are searching for a **page number** in the **entire book**. And after **5 or 10 more steps**, you find the page 253 in Chapter 5. - + After this, even though this book is not a dictionary and has some particular content, you were able to **find the section** in the book that talks about a "**database**" in a **few steps** (say 10 or 20, instead of reading all the 500 pages). @@ -261,46 +257,23 @@ The change in code is underwhelming, it's very simple. 😆 Here's the `Hero` model we had before: -```Python hl_lines="8" -{!./docs_src/tutorial/where/tutorial001.py[ln:1-10]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/where/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/where/tutorial001_py310.py ln[1:8] hl[6] *} Let's now update it to tell **SQLModel** to create an index for the `name` field when creating the table: -```Python hl_lines="8" -{!./docs_src/tutorial/indexes/tutorial001.py[ln:1-10]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/indexes/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/indexes/tutorial001_py310.py ln[1:8] hl[6] *} We use the same `Field()` again as we did before, and set `index=True`. That's it! 🚀 Notice that we didn't set an argument of `default=None` or anything similar. This means that **SQLModel** (thanks to Pydantic) will keep it as a **required** field. -!!! info - SQLModel (actually SQLAlchemy) will **automatically generate the index name** for you. +/// info + +SQLModel (actually SQLAlchemy) will **automatically generate the index name** for you. - In this case the generated name would be `ix_hero_name`. +In this case the generated name would be `ix_hero_name`. + +/// ## Query Data @@ -310,22 +283,7 @@ The SQL database will figure it out **automatically**. ✨ This is great because it means that indexes are very **simple to use**. But it might also feel counterintuitive at first, as you are **not doing anything** explicitly in the code to make it obvious that the index is useful, it all happens in the database behind the scenes. -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/indexes/tutorial001.py[ln:36-41]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/indexes/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/indexes/tutorial001_py310.py ln[34:39] hl[36] *} This is exactly the same code as we had before, but now the database will **use the index** underneath. @@ -342,10 +300,10 @@ $ python app.py // Create the table CREATE TABLE hero ( - id INTEGER, - name VARCHAR NOT NULL, - secret_name VARCHAR NOT NULL, - age INTEGER, + id INTEGER, + name VARCHAR NOT NULL, + secret_name VARCHAR NOT NULL, + age INTEGER, PRIMARY KEY (id) ) @@ -353,8 +311,8 @@ CREATE TABLE hero ( CREATE INDEX ix_hero_name ON hero (name) // The SELECT with WHERE looks the same -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.name = ? INFO Engine [no key 0.00014s] ('Deadpond',) @@ -368,20 +326,7 @@ secret_name='Dive Wilson' age=None id=1 name='Deadpond' We are going to query the `hero` table doing comparisons on the `age` field too, so we should **define an index** for that one as well: -```Python hl_lines="10" -{!./docs_src/tutorial/indexes/tutorial002.py[ln:1-10]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/indexes/tutorial002.py!} -``` - -
+{* ./docs_src/tutorial/indexes/tutorial002_py310.py ln[1:8] hl[8] *} In this case, we want the default value of `age` to continue being `None`, so we set `default=None` when using `Field()`. diff --git a/docs/tutorial/insert.md b/docs/tutorial/insert.md index 5947e1e5dc..502a15cb87 100644 --- a/docs/tutorial/insert.md +++ b/docs/tutorial/insert.md @@ -21,20 +21,38 @@ Here's a reminder of how the table would look like, this is the data we want to ## Create Table and Database -We will continue from where we left of in the last chapter. +We will continue from where we left off in the last chapter. This is the code we had to create the database and table, nothing new here: +//// tab | Python 3.10+ + +```{.python .annotate hl_lines="20" } +{!./docs_src/tutorial/create_db_and_table/tutorial003_py310.py[ln:1-18]!} + +# More code here later 👈 + +{!./docs_src/tutorial/create_db_and_table/tutorial003_py310.py[ln:21-22]!} +``` + +{!./docs_src/tutorial/create_db_and_table/annotations/en/tutorial003.md!} + +//// + +//// tab | Python 3.9+ + ```{.python .annotate hl_lines="22" } -{!./docs_src/tutorial/create_db_and_table/tutorial003.py[ln:1-20]!} +{!./docs_src/tutorial/create_db_and_table/tutorial003_py39.py[ln:1-20]!} # More code here later 👈 -{!./docs_src/tutorial/create_db_and_table/tutorial003.py[ln:23-24]!} +{!./docs_src/tutorial/create_db_and_table/tutorial003_py39.py[ln:23-24]!} ``` {!./docs_src/tutorial/create_db_and_table/annotations/en/tutorial003.md!} +//// + Now that we can create the database and the table, we will continue from this point and add more code on the same file to create the data. ## Create Data with SQL @@ -70,8 +88,11 @@ You can try that SQL statement in **DB Explorer for SQLite**. Make sure to open the same database we already created by clicking Open Database and selecting the same `database.db` file. -!!! tip - If you don't have that `database.db` file with the table `hero`, you can re-create it by running the Python program at the top. 👆 +/// tip + +If you don't have that `database.db` file with the table `hero`, you can re-create it by running the Python program at the top. 👆 + +/// Then go to the Execute SQL tab and copy the SQL from above. @@ -124,27 +145,15 @@ So, the first step is to simply create an instance of `Hero`. We'll create 3 right away, for the 3 heroes: -```Python -# Code above omitted 👆 - -{!./docs_src/tutorial/insert/tutorial002.py[ln:23-26]!} +{* ./docs_src/tutorial/insert/tutorial002_py310.py ln[21:24] *} -# More code here later 👇 -``` +/// tip -
-👀 Full file preview +The code above in this file (the omitted code) is just the same code that you see at the top of this chapter. -```Python -{!./docs_src/tutorial/insert/tutorial002.py!} -``` +The same code we used before to create the `Hero` model. -
- -!!! tip - The code above in this file (the omitted code) is just the same code that you see at the top of this chapter. - - The same code we used before to create the `Hero` model. +/// We are putting that in a function `create_heroes()`, to call it later once we finish it. @@ -168,64 +177,25 @@ We would re-use the same **engine** in all the code, everywhere in the applicati The first step is to import the `Session` class: -```Python hl_lines="3" -{!./docs_src/tutorial/insert/tutorial001.py[ln:1-3]!} - -# Code below ommitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/insert/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/insert/tutorial001_py310.py ln[1] hl[1] *} Then we can create a new session: -```Python hl_lines="8" -# Code above omitted 👆 - -{!./docs_src/tutorial/insert/tutorial001.py[ln:23-28]!} +{* ./docs_src/tutorial/insert/tutorial001_py310.py ln[21:26] hl[26] *} -# More code here later 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/insert/tutorial001.py!} -``` +The new `Session` takes an `engine` as a parameter. And it will use the **engine** underneath. -
+/// tip -The new `Session` takes an `engine` as a parameter. And it will use the **engine** underneath. +We will see a better way to create a **session** using a `with` block later. -!!! tip - We will see a better way to create a **session** using a `with` block later. +/// ## Add Model Instances to the Session Now that we have some hero model instances (some objects in memory) and a **session**, the next step is to add them to the session: -```Python hl_lines="9-11" -# Code above omitted 👆 -{!./docs_src/tutorial/insert/tutorial001.py[ln:23-32]!} - -# More code here later 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/insert/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/insert/tutorial001_py310.py ln[21:30] hl[28:30] *} By this point, our heroes are *not* stored in the database yet. @@ -237,30 +207,19 @@ And once we are ready, we can **commit** those changes, and then the **session** This makes the interactions with the database more efficient (plus some extra benefits). -!!! info "Technical Details" - The session will create a new transaction and execute all the SQL code in that transaction. +/// info | Technical Details - This ensures that the data is saved in a single batch, and that it will all succeed or all fail, but it won't leave the database in a broken state. +The session will create a new transaction and execute all the SQL code in that transaction. -## Commit the Session Changes - -Now that we have the heroes in the **session** and that we are ready to save all that to the database, we can **commit** the changes: +This ensures that the data is saved in a single batch, and that it will all succeed or all fail, but it won't leave the database in a broken state. -```Python hl_lines="13" -# Code above omitted 👆 -{!./docs_src/tutorial/insert/tutorial001.py[ln:23-34]!} +/// -# More code here later 👇 -``` - -
-👀 Full file preview +## Commit the Session Changes -```Python -{!./docs_src/tutorial/insert/tutorial001.py!} -``` +Now that we have the heroes in the **session** and that we are ready to save all that to the database, we can **commit** the changes: -
+{* ./docs_src/tutorial/insert/tutorial001_py310.py ln[21:32] hl[32] *} Once this line is executed, the **session** will use the **engine** to save all the data in the database by sending the corresponding SQL. @@ -287,37 +246,11 @@ if __name__ == "__main__": But to keep things a bit more organized, let's instead create a new function `main()` that will contain all the code that should be executed when called as an independent script, and we can put there the previous function `create_db_and_tables()`, and add the new function `create_heroes()`: -```Python hl_lines="2 4" -# Code above omitted 👆 -{!./docs_src/tutorial/insert/tutorial002.py[ln:36-38]!} - -# More code here later 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/insert/tutorial002.py!} -``` - -
+{* ./docs_src/tutorial/insert/tutorial002_py310.py ln[34:36] hl[34,36] *} And then we can call that single `main()` function from that main block: -```Python hl_lines="8" -# Code above omitted 👆 -{!./docs_src/tutorial/insert/tutorial002.py[ln:36-42]!} -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/insert/tutorial002.py!} -``` - -
+{* ./docs_src/tutorial/insert/tutorial002_py310.py ln[34:40] hl[40] *} By having everything that should happen when called as a script in a single function, we can easily add more code later on. @@ -372,22 +305,7 @@ The **session** holds some resources, like connections from the engine. So once we are done with the session, we should **close** it to make it release those resources and finish its cleanup: -```Python hl_lines="16" -# Code above omitted 👆 - -{!./docs_src/tutorial/insert/tutorial001.py[ln:23-36]!} - -# More code here later 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/insert/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/insert/tutorial001_py310.py ln[21:34] hl[34] *} But what happens if we forget to close the session? @@ -401,19 +319,7 @@ It's good to know how the `Session` works and how to create and close it manuall But there's a better way to handle the session, using a `with` block: -```Python hl_lines="7-12" -# Code above omitted 👆 -{!./docs_src/tutorial/insert/tutorial002.py[ln:23-33]!} -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/insert/tutorial002.py!} -``` - -
+{* ./docs_src/tutorial/insert/tutorial002_py310.py ln[21:31] hl[26:31] *} This is the same as creating the session manually and then manually closing it. But here, using a `with` block, it will be automatically created when **starting** the `with` block and assigned to the variable `session`, and it will be automatically closed after the `with` block is **finished**. @@ -423,18 +329,35 @@ And it will work even if there's an exception in the code. 😎 Let's give this whole file a final look. 🔍 -You already know all the first part creating the `Hero` model class, the **engine**, and creating the database and table. +You already know all of the first part for creating the `Hero` model class, the **engine**, and creating the database and table. Let's focus on the new code: +//// tab | Python 3.10+ + ```{.python .annotate } -{!./docs_src/tutorial/insert/tutorial003.py!} +{!./docs_src/tutorial/insert/tutorial003_py310.py!} ``` {!./docs_src/tutorial/insert/annotations/en/tutorial003.md!} -!!! tip - Review what each line does by clicking each number bubble in the code. 👆 +//// + +//// tab | Python 3.9+ + +```{.python .annotate } +{!./docs_src/tutorial/insert/tutorial003_py39.py!} +``` + +{!./docs_src/tutorial/insert/annotations/en/tutorial003.md!} + +//// + +/// tip + +Review what each line does by clicking each number bubble in the code. 👆 + +/// You can now put it in a `app.py` file and run it with Python. And you will see an output like the one shown above. diff --git a/docs/tutorial/limit-and-offset.md b/docs/tutorial/limit-and-offset.md index 3fb001cf97..215f57d1cd 100644 --- a/docs/tutorial/limit-and-offset.md +++ b/docs/tutorial/limit-and-offset.md @@ -6,7 +6,7 @@ And you also know how to get multiple rows while filtering them using `.where()` Now let's see how to get only a **range of results**. -table with first 3 rows selected +table with first 3 rows selected ## Create Data @@ -14,43 +14,13 @@ We will continue with the same code as before, but we'll modify it a little the Again, we will create several heroes to have some data to select from: -```Python hl_lines="4-10" -# Code above omitted 👆 - -{!./docs_src/tutorial/offset_and_limit/tutorial001.py[ln:23-41]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/offset_and_limit/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/offset_and_limit/tutorial001_py310.py ln[21:39] hl[22:28] *} ## Review Select All This is the code we had to select all the heroes in the `select()` examples: -```Python hl_lines="3-8" -# Code above omitted 👆 - -{!./docs_src/tutorial/select/tutorial003.py[ln:36-41]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/select/tutorial003.py!} -``` - -
+{* ./docs_src/tutorial/select/tutorial003_py310.py ln[34:39] hl[34:39] *} But this would get us **all** the heroes at the same time, in a database that could have thousands, that could be problematic. @@ -58,28 +28,13 @@ But this would get us **all** the heroes at the same time, in a database that co We currently have 7 heroes in the database. But we could as well have thousands, so let's limit the results to get only the first 3: -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/offset_and_limit/tutorial001.py[ln:44-49]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/offset_and_limit/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/offset_and_limit/tutorial001_py310.py ln[42:47] hl[44] *} The special **select** object we get from `select()` also has a method `.limit()` that we can use to limit the results to a certain number. In this case, instead of getting all the 7 rows, we are limiting them to only get the first 3. -table with first 3 rows selected +table with first 3 rows selected ## Run the Program on the Command Line @@ -93,7 +48,7 @@ $ python app.py // Previous output omitted 🙈 // Select with LIMIT -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age FROM hero LIMIT ? OFFSET ? INFO Engine [no key 0.00014s] (3, 0) @@ -110,8 +65,11 @@ INFO Engine [no key 0.00014s] (3, 0) Great! We got only 3 heroes as we wanted. -!!! tip - We will check out that SQL code more in a bit. +/// tip + +We will check out that SQL code more in a bit. + +/// ## Select with Offset and Limit @@ -119,33 +77,21 @@ Now we can limit the results to get only the first 3. But imagine we are in a user interface showing the results in batches of 3 heroes at a time. -!!! tip - This is commonly called "pagination". Because the user interface would normally show a "page" of a predefined number of heroes at a time. - - And then you can interact with the user interface to get the next page, and so on. - -How do we get the next 3? - -table with next rows selected, from 4 to 6 +/// tip -We can use `.offset()`: +This is commonly called "pagination". Because the user interface would normally show a "page" of a predefined number of heroes at a time. -```Python hl_lines="5" -# Code above omitted 👆 +And then you can interact with the user interface to get the next page, and so on. -{!./docs_src/tutorial/offset_and_limit/tutorial002.py[ln:44-49]!} +/// -# Code below omitted 👇 -``` +How do we get the next 3? -
-👀 Full file preview +table with next rows selected, from 4 to 6 -```Python -{!./docs_src/tutorial/offset_and_limit/tutorial002.py!} -``` +We can use `.offset()`: -
+{* ./docs_src/tutorial/offset_and_limit/tutorial002_py310.py ln[42:47] hl[44] *} The way this works is that the special **select** object we get from `select()` has methods like `.where()`, `.offset()` and `.limit()`. @@ -165,7 +111,7 @@ $python app.py // Previous output omitted 🙈 // Select with LIMIT and OFFSET -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age FROM hero LIMIT ? OFFSET ? INFO Engine [no key 0.00020s] (3, 3) @@ -184,26 +130,11 @@ INFO Engine [no key 0.00020s] (3, 3) Then to get the next batch of 3 rows we would offset all the ones we already saw, the first 6: -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/offset_and_limit/tutorial003.py[ln:44-49]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/offset_and_limit/tutorial003.py!} -``` - -
+{* ./docs_src/tutorial/offset_and_limit/tutorial003_py310.py ln[42:47] hl[44] *} The database right now has **only 7 rows**, so this query can only get 1 row. -table with the last row (7th) selected +table with the last row (7th) selected But don't worry, the database won't throw an error trying to get 3 rows when there's only one (as would happen with a Python list). @@ -221,7 +152,7 @@ $ python app.py // Previous output omitted 🙈 // Select last batch with LIMIT and OFFSET -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age FROM hero LIMIT ? OFFSET ? INFO Engine [no key 0.00038s] (3, 6) @@ -241,7 +172,7 @@ You probably noticed the new SQL keywords `LIMIT` and `OFFSET`. You can use them in SQL, at the end of the other parts: ```SQL -SELECT id, name, secret_name, age +SELECT id, name, secret_name, age FROM hero LIMIT 3 OFFSET 6 ``` @@ -254,28 +185,13 @@ If you try that in **DB Browser for SQLite**, you will get the same result: Of course, you can also combine `.limit()` and `.offset()` with `.where()` and other methods you will learn about later: -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/offset_and_limit/tutorial004.py[ln:44-49]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/offset_and_limit/tutorial004.py!} -``` - -
+{* ./docs_src/tutorial/offset_and_limit/tutorial004_py310.py ln[42:47] hl[44] *} -## Run the Program with Limit and Where on the Command Line +## Run the Program with Limit, Offset, and Where on the Command Line If we run it on the command line, it will find all the heroes in the database with an age above 32. That would normally be 4 heroes. -But we are limiting the results to only get the first 3: +But we are starting to include after an offset of 1 (so we don't count the first one), and we are limiting the results to only get the first 2 after that:
@@ -284,18 +200,17 @@ $ python app.py // Previous output omitted 🙈 -// Select with WHERE and LIMIT -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +// Select with WHERE and LIMIT and OFFSET +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.age > ? LIMIT ? OFFSET ? -INFO Engine [no key 0.00022s] (32, 3, 0) +INFO Engine [no key 0.00022s] (32, 2, 1) -// Print the heroes received, only 3 +// Print the heroes received, only 2 [ - Hero(age=35, secret_name='Trevor Challa', id=5, name='Black Lion'), - Hero(age=36, secret_name='Steve Weird', id=6, name='Dr. Weird'), - Hero(age=48, secret_name='Tommy Sharp', id=3, name='Rusty-Man') + Hero(age=36, id=6, name='Dr. Weird', secret_name='Steve Weird'), + Hero(age=48, id=3, name='Rusty-Man', secret_name='Tommy Sharp') ] ``` diff --git a/docs/tutorial/many-to-many/create-data.md b/docs/tutorial/many-to-many/create-data.md index 22afb7ce7e..3c8c0cfa6a 100644 --- a/docs/tutorial/many-to-many/create-data.md +++ b/docs/tutorial/many-to-many/create-data.md @@ -4,39 +4,17 @@ Let's continue from where we left and create some data. We'll create data for this same **many-to-many** relationship with a link table: -many-to-many table relationships +many-to-many table relationships We'll continue from where we left off with the previous code. -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/many_to_many/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/many_to_many/tutorial001_py310.py ln[0] *} ## Create Heroes As we have done before, we'll create a function `create_heroes()` and we'll create some teams and heroes in it: -```Python hl_lines="11" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial001.py[ln:42-60]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/many_to_many/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/many_to_many/tutorial001_py310.py ln[36:54] hl[44] *} This is very similar to what we have done before. @@ -50,43 +28,13 @@ See how **Deadpond** now belongs to the two teams? Now let's do as we have done before, `commit` the **session**, `refresh` the data, and print it: -```Python hl_lines="22-25 27-29 31-36" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial001.py[ln:42-75]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/many_to_many/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/many_to_many/tutorial001_py310.py ln[36:69] hl[55:58,60:62,64:69] *} ## Add to Main As before, add the `create_heroes()` function to the `main()` function to make sure it is called when running this program from the command line: -```Python hl_lines="22-25 27-29 31-36" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial001.py[ln:78-80]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/many_to_many/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/many_to_many/tutorial001_py310.py ln[72:74] *} ## Run the Program @@ -110,7 +58,7 @@ INFO Engine INSERT INTO hero (name, secret_name, age) VALUES (?, ?, ?) INFO Engine [cached since 0.002541s ago] ('Spider-Boy', 'Pedro Parqueador', None) // Insert the team data second INFO Engine INSERT INTO team (name, headquarters) VALUES (?, ?) -INFO Engine [generated in 0.00037s] ('Z-Force', 'Sister Margaret’s Bar') +INFO Engine [generated in 0.00037s] ('Z-Force', 'Sister Margaret's Bar') INFO Engine INSERT INTO team (name, headquarters) VALUES (?, ?) INFO Engine [cached since 0.001239s ago] ('Preventers', 'Sharp Tower') // Insert the link data last, to be able to re-use the created IDs @@ -122,16 +70,16 @@ INFO Engine COMMIT // Automatically start a new transaction INFO Engine BEGIN (implicit) // Refresh the data -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.id = ? INFO Engine [generated in 0.00019s] (1,) -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.id = ? INFO Engine [cached since 0.001959s ago] (2,) -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.id = ? INFO Engine [cached since 0.003215s ago] (3,) @@ -139,20 +87,20 @@ INFO Engine [cached since 0.003215s ago] (3,) Deadpond: name='Deadpond' age=None id=1 secret_name='Dive Wilson' // Accessing the .team attribute triggers a refresh -INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters -FROM team, heroteamlink +INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters +FROM team, heroteamlink WHERE ? = heroteamlink.hero_id AND team.id = heroteamlink.team_id INFO Engine [generated in 0.00025s] (1,) // Print Deadpond's teams, 2 teams! 🎉 -Deadpond teams: [Team(id=1, name='Z-Force', headquarters='Sister Margaret’s Bar'), Team(id=2, name='Preventers', headquarters='Sharp Tower')] +Deadpond teams: [Team(id=1, name='Z-Force', headquarters='Sister Margaret's Bar'), Team(id=2, name='Preventers', headquarters='Sharp Tower')] // Print Rusty-Man Rusty-Man: name='Rusty-Man' age=48 id=2 secret_name='Tommy Sharp' // Accessing the .team attribute triggers a refresh -INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters -FROM team, heroteamlink +INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters +FROM team, heroteamlink WHERE ? = heroteamlink.hero_id AND team.id = heroteamlink.team_id INFO Engine [cached since 0.001716s ago] (2,) @@ -163,8 +111,8 @@ Rusty-Man Teams: [Team(id=2, name='Preventers', headquarters='Sharp Tower')] Spider-Boy: name='Spider-Boy' age=None id=3 secret_name='Pedro Parqueador' // Accessing the .team attribute triggers a refresh -INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters -FROM team, heroteamlink +INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters +FROM team, heroteamlink WHERE ? = heroteamlink.hero_id AND team.id = heroteamlink.team_id INFO Engine [cached since 0.002739s ago] (3,) @@ -179,4 +127,4 @@ INFO Engine ROLLBACK ## Recap -After setting up the model link, using it with **relationship attributes** is fairly straighforward, just Python objects. ✨ +After setting up the model link, using it with **relationship attributes** is fairly straightforward, just Python objects. ✨ diff --git a/docs/tutorial/many-to-many/create-models-with-link.md b/docs/tutorial/many-to-many/create-models-with-link.md index 2b5fb8cf73..af6563e1ab 100644 --- a/docs/tutorial/many-to-many/create-models-with-link.md +++ b/docs/tutorial/many-to-many/create-models-with-link.md @@ -2,7 +2,7 @@ We'll now support **many-to-many** relationships using a **link table** like this: -many-to-many table relationships +many-to-many table relationships Let's start by defining the class models, including the **link table** model. @@ -12,20 +12,7 @@ As we want to support a **many-to-many** relationship, now we need a **link tabl We can create it just as any other **SQLModel**: -```Python hl_lines="6-12" -{!./docs_src/tutorial/many_to_many/tutorial001.py[ln:1-12]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/many_to_many/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/many_to_many/tutorial001_py310.py ln[1:6] hl[4:6] *} This is a **SQLModel** class model table like any other. @@ -39,24 +26,9 @@ And **both fields are primary keys**. We hadn't used this before. 🤓 Let's see the `Team` model, it's almost identical as before, but with a little change: -```Python hl_lines="8" -# Code above ommited 👆 - -{!./docs_src/tutorial/many_to_many/tutorial001.py[ln:15-20]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/many_to_many/tutorial001.py!} -``` +{* ./docs_src/tutorial/many_to_many/tutorial001_py310.py ln[9:14] hl[14] *} -
- -The **relationship attribute `heroes`** is still a list of heroes, annotatted as `List["Hero"]`. Again, we use `"Hero"` in quotes because we haven't declared that class yet by this point in the code (but as you know, editors and **SQLModel** understand that). +The **relationship attribute `heroes`** is still a list of heroes, annotated as `list["Hero"]`. Again, we use `"Hero"` in quotes because we haven't declared that class yet by this point in the code (but as you know, editors and **SQLModel** understand that). We use the same **`Relationship()`** function. @@ -68,28 +40,13 @@ And here's the important part to allow the **many-to-many** relationship, we use Let's see the other side, here's the `Hero` model: -```Python hl_lines="9" -# Code above ommited 👆 - -{!./docs_src/tutorial/many_to_many/tutorial001.py[ln:23-29]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/many_to_many/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/many_to_many/tutorial001_py310.py ln[17:23] hl[23] *} We **removed** the previous `team_id` field (column) because now the relationship is done via the link table. 🔥 The relationship attribute is now named **`teams`** instead of `team`, as now we support multiple teams. -It is no longer an `Optional[Team]` but a list of teams, annotated as **`List[Team]`**. +It no longer has a type of `Team | None` but a list of teams, the type is now declared as **`list[Team]`**. We are using the **`Relationship()`** here too. @@ -101,44 +58,11 @@ And now we have a **`link_model=HeroTeamLink`**. ✨ The same as before, we will have the rest of the code to create the **engine**, and a function to create all the tables `create_db_and_tables()`. -```Python hl_lines="9" -# Code above ommited 👆 - -{!./docs_src/tutorial/many_to_many/tutorial001.py[ln:32-39]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/many_to_many/tutorial001.py!} -``` - -
- +{* ./docs_src/tutorial/many_to_many/tutorial001_py310.py ln[26:33] hl[32] *} And as in previous examples, we will add that function to a function `main()`, and we will call that `main()` function in the main block: -```Python hl_lines="4" -# Code above ommited 👆 - -{!./docs_src/tutorial/many_to_many/tutorial001.py[ln:78-79]!} - # We will do more stuff here later 👈 - -{!./docs_src/tutorial/many_to_many/tutorial001.py[ln:83-84]!} -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/many_to_many/tutorial001.py!} -``` - -
- +{* ./docs_src/tutorial/many_to_many/tutorial001_py310.py ln[72:73,77:78] hl[73] *} ## Run the Code @@ -149,37 +73,37 @@ If you run the code in the command line, it would output: ```console $ python app.py -// Boilerplate ommited 😉 +// Boilerplate omitted 😉 -INFO Engine +INFO Engine CREATE TABLE team ( - id INTEGER, - name VARCHAR NOT NULL, - headquarters VARCHAR NOT NULL, + id INTEGER, + name VARCHAR NOT NULL, + headquarters VARCHAR NOT NULL, PRIMARY KEY (id) ) INFO Engine [no key 0.00033s] () -INFO Engine +INFO Engine CREATE TABLE hero ( - id INTEGER, - name VARCHAR NOT NULL, - secret_name VARCHAR NOT NULL, - age INTEGER, + id INTEGER, + name VARCHAR NOT NULL, + secret_name VARCHAR NOT NULL, + age INTEGER, PRIMARY KEY (id) ) INFO Engine [no key 0.00016s] () -INFO Engine +INFO Engine // Our shinny new link table ✨ CREATE TABLE heroteamlink ( - team_id INTEGER, - hero_id INTEGER, - PRIMARY KEY (team_id, hero_id), - FOREIGN KEY(team_id) REFERENCES team (id), + team_id INTEGER, + hero_id INTEGER, + PRIMARY KEY (team_id, hero_id), + FOREIGN KEY(team_id) REFERENCES team (id), FOREIGN KEY(hero_id) REFERENCES hero (id) ) diff --git a/docs/tutorial/many-to-many/index.md b/docs/tutorial/many-to-many/index.md index 24d7824fe0..5cb3067e4a 100644 --- a/docs/tutorial/many-to-many/index.md +++ b/docs/tutorial/many-to-many/index.md @@ -26,12 +26,15 @@ The `team` table looks like this: 1PreventersSharp Tower -2Z-ForceSister Margaret’s Bar +2Z-ForceSister Margaret's Bar -!!! tip - Notice that it doesn't have any foreign key to other tables. +/// tip + +Notice that it doesn't have any foreign key to other tables. + +/// And the `hero` table looks like this: @@ -54,13 +57,13 @@ We have a column in the `hero` table for the `team_id` that points to the ID of This is how we connect each `hero` with a `team`: -table relationships +table relationships Notice that each hero can only have **one** connection. But each team can receive **many** connections. In particular, the team **Preventers** has two heroes. ## Introduce Many-to-Many -But let's say that as **Deadpond** is a great chracter, they recruit him to the new **Preventers** team, but he's still part of the **Z-Force** team too. +But let's say that as **Deadpond** is a great character, they recruit him to the new **Preventers** team, but he's still part of the **Z-Force** team too. So, now, we need to be able to have a hero that is connected to **many** teams. And then, each team, should still be able to receive **many** heroes. So we need a **Many-to-Many** relationship. @@ -80,7 +83,7 @@ As this will represent the **hero-team-link**, let's call the table `heroteamlin It would look like this: -many-to-many table relationships +many-to-many table relationships Notice that now the table `hero` **doesn't have a `team_id`** column anymore, it is replaced by this link table. @@ -106,19 +109,22 @@ Specifically, the new link table `heroteamlink` would be: -!!! info - Other names used for this **link table** are: +/// info + +Other names used for this **link table** are: + +* association table +* secondary table +* junction table +* intermediate table +* join table +* through table +* relationship table +* connection table - * association table - * secondary table - * junction table - * intermediate table - * join table - * through table - * relationship table - * connection table +I'm using the term "link table" because it's short, doesn't collide with other terms already used (e.g. "relationship"), it's easy to remember how to write it, etc. - I'm using the term "link table" because it's short, doesn't collide with other terms already used (e.g. "relationship"), it's easy to remember how to write it, etc. +/// ## Link Primary Key diff --git a/docs/tutorial/many-to-many/link-with-extra-fields.md b/docs/tutorial/many-to-many/link-with-extra-fields.md index 9c3309da91..7c7756bcd2 100644 --- a/docs/tutorial/many-to-many/link-with-extra-fields.md +++ b/docs/tutorial/many-to-many/link-with-extra-fields.md @@ -18,8 +18,11 @@ A row in the table `heroteamlink` points to **one** particular hero, but a singl And also, the same row in the table `heroteamlink` points to **one** team, but a single team can be connected to **many** hero-team links, so it's also **one-to-many**. -!!! tip - The previous many-to-many relationship was also just two one-to-many relationships combined, but now it's going to be much more explicit. +/// tip + +The previous many-to-many relationship was also just two one-to-many relationships combined, but now it's going to be much more explicit. + +/// ## Update Link Model @@ -29,32 +32,20 @@ We will add a new field `is_training`. And we will also add two **relationship attributes**, for the linked `team` and `hero`: -```Python hl_lines="10 12-13" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial003.py[ln:6-16]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/many_to_many/tutorial003.py!} -``` - -
+{* ./docs_src/tutorial/many_to_many/tutorial003_py310.py ln[4:10] hl[7,9:10] *} The new **relationship attributes** have their own `back_populates` pointing to new relationship attributes we will create in the `Hero` and `Team` models: * `team`: has `back_populates="hero_links"`, because in the `Team` model, the attribute will contain the links to the **team's heroes**. * `hero`: has `back_populates="team_links"`, because in the `Hero` model, the attribute will contain the links to the **hero's teams**. -!!! info - In SQLAlchemy this is called an Association Object or Association Model. +/// info - I'm calling it **Link Model** just because that's easier to write avoiding typos. But you are also free to call it however you want. 😉 +In SQLAlchemy this is called an Association Object or Association Model. + +I'm calling it **Link Model** just because that's easier to write avoiding typos. But you are also free to call it however you want. 😉 + +/// ## Update Team Model @@ -62,22 +53,7 @@ Now let's update the `Team` model. We no longer have the `heroes` relationship attribute, and instead we have the new `hero_links` attribute: -```Python hl_lines="8" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial003.py[ln:19-24]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/many_to_many/tutorial003.py!} -``` - -
+{* ./docs_src/tutorial/many_to_many/tutorial003_py310.py ln[13:18] hl[18] *} ## Update Hero Model @@ -85,22 +61,7 @@ The same with the `Hero` model. We change the `teams` relationship attribute for `team_links`: -```Python hl_lines="9" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial003.py[ln:27-33]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/many_to_many/tutorial003.py!} -``` - -
+{* ./docs_src/tutorial/many_to_many/tutorial003_py310.py ln[21:27] hl[27] *} ## Create Relationships @@ -108,22 +69,7 @@ Now the process to create relationships is very similar. But now we create the **explicit link models** manually, pointing to their hero and team instances, and specifying the additional link data (`is_training`): -```Python hl_lines="21-30 32-35" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial003.py[ln:46-85]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/many_to_many/tutorial003.py!} -``` - -
+{* ./docs_src/tutorial/many_to_many/tutorial003_py310.py ln[40:79] hl[58:67,69:72] *} We are just adding the link model instances to the session, because the link model instances are connected to the heroes and teams, they will be also automatically included in the session when we commit. @@ -151,7 +97,7 @@ INFO Engine [cached since 0.001858s ago] ('Rusty-Man', 'Tommy Sharp', 48) // Insert the teams INFO Engine INSERT INTO team (name, headquarters) VALUES (?, ?) -INFO Engine [generated in 0.00019s] ('Z-Force', 'Sister Margaret’s Bar') +INFO Engine [generated in 0.00019s] ('Z-Force', 'Sister Margaret's Bar') INFO Engine INSERT INTO team (name, headquarters) VALUES (?, ?) INFO Engine [cached since 0.0007985s ago] ('Preventers', 'Sharp Tower') @@ -165,16 +111,16 @@ INFO Engine COMMIT INFO Engine BEGIN (implicit) // Automatically fetch the data on attribute access -INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters -FROM team +INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters +FROM team WHERE team.id = ? INFO Engine [generated in 0.00028s] (1,) -INFO Engine SELECT heroteamlink.team_id AS heroteamlink_team_id, heroteamlink.hero_id AS heroteamlink_hero_id, heroteamlink.is_training AS heroteamlink_is_training -FROM heroteamlink +INFO Engine SELECT heroteamlink.team_id AS heroteamlink_team_id, heroteamlink.hero_id AS heroteamlink_hero_id, heroteamlink.is_training AS heroteamlink_is_training +FROM heroteamlink WHERE ? = heroteamlink.team_id INFO Engine [generated in 0.00026s] (1,) -INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age -FROM hero +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age +FROM hero WHERE hero.id = ? INFO Engine [generated in 0.00024s] (1,) @@ -182,12 +128,12 @@ INFO Engine [generated in 0.00024s] (1,) Z-Force hero: name='Deadpond' age=None id=1 secret_name='Dive Wilson' is training: False // Automatically fetch the data on attribute access -INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters -FROM team +INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters +FROM team WHERE team.id = ? INFO Engine [cached since 0.008822s ago] (2,) -INFO Engine SELECT heroteamlink.team_id AS heroteamlink_team_id, heroteamlink.hero_id AS heroteamlink_hero_id, heroteamlink.is_training AS heroteamlink_is_training -FROM heroteamlink +INFO Engine SELECT heroteamlink.team_id AS heroteamlink_team_id, heroteamlink.hero_id AS heroteamlink_hero_id, heroteamlink.is_training AS heroteamlink_is_training +FROM heroteamlink WHERE ? = heroteamlink.team_id INFO Engine [cached since 0.005778s ago] (2,) @@ -195,8 +141,8 @@ INFO Engine [cached since 0.005778s ago] (2,) Preventers hero: name='Deadpond' age=None id=1 secret_name='Dive Wilson' is training: True // Automatically fetch the data on attribute access -INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age -FROM hero +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age +FROM hero WHERE hero.id = ? INFO Engine [cached since 0.004196s ago] (2,) @@ -204,8 +150,8 @@ INFO Engine [cached since 0.004196s ago] (2,) Preventers hero: name='Spider-Boy' age=None id=2 secret_name='Pedro Parqueador' is training: True // Automatically fetch the data on attribute access -INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age -FROM hero +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age +FROM hero WHERE hero.id = ? INFO Engine [cached since 0.006005s ago] (3,) @@ -221,22 +167,7 @@ Now, to add a new relationship, we have to create a new `HeroTeamLink` instance Here we do that in the `update_heroes()` function: -```Python hl_lines="10-15" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial003.py[ln:88-103]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/many_to_many/tutorial003.py!} -``` - -
+{* ./docs_src/tutorial/many_to_many/tutorial003_py310.py ln[82:97] hl[89:94] *} ## Run the Program with the New Relationship @@ -253,14 +184,14 @@ $ python app.py INFO Engine BEGIN (implicit) // Select the hero -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.name = ? INFO Engine [no key 0.00014s] ('Spider-Boy',) // Select the team -INFO Engine SELECT team.id, team.name, team.headquarters -FROM team +INFO Engine SELECT team.id, team.name, team.headquarters +FROM team WHERE team.name = ? INFO Engine [no key 0.00012s] ('Z-Force',) @@ -269,18 +200,18 @@ INFO Engine INSERT INTO heroteamlink (team_id, hero_id, is_training) VALUES (?, INFO Engine [generated in 0.00023s] (1, 2, 1) // Automatically refresh the data on attribute access -INFO Engine SELECT heroteamlink.team_id AS heroteamlink_team_id, heroteamlink.hero_id AS heroteamlink_hero_id, heroteamlink.is_training AS heroteamlink_is_training -FROM heroteamlink +INFO Engine SELECT heroteamlink.team_id AS heroteamlink_team_id, heroteamlink.hero_id AS heroteamlink_hero_id, heroteamlink.is_training AS heroteamlink_is_training +FROM heroteamlink WHERE ? = heroteamlink.team_id INFO Engine [cached since 0.01514s ago] (1,) INFO Engine COMMIT INFO Engine BEGIN (implicit) -INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age -FROM hero +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age +FROM hero WHERE hero.id = ? INFO Engine [cached since 0.08953s ago] (2,) -INFO Engine SELECT heroteamlink.team_id AS heroteamlink_team_id, heroteamlink.hero_id AS heroteamlink_hero_id, heroteamlink.is_training AS heroteamlink_is_training -FROM heroteamlink +INFO Engine SELECT heroteamlink.team_id AS heroteamlink_team_id, heroteamlink.hero_id AS heroteamlink_hero_id, heroteamlink.is_training AS heroteamlink_is_training +FROM heroteamlink WHERE ? = heroteamlink.hero_id INFO Engine [generated in 0.00018s] (2,) @@ -291,18 +222,18 @@ Updated Spider-Boy's Teams: [ ] // Automatically refresh team data on attribute access -INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters -FROM team +INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters +FROM team WHERE team.id = ? INFO Engine [cached since 0.1084s ago] (1,) -INFO Engine SELECT heroteamlink.team_id AS heroteamlink_team_id, heroteamlink.hero_id AS heroteamlink_hero_id, heroteamlink.is_training AS heroteamlink_is_training -FROM heroteamlink +INFO Engine SELECT heroteamlink.team_id AS heroteamlink_team_id, heroteamlink.hero_id AS heroteamlink_hero_id, heroteamlink.is_training AS heroteamlink_is_training +FROM heroteamlink WHERE ? = heroteamlink.team_id INFO Engine [cached since 0.1054s ago] (1,) // Print team hero links Z-Force heroes: [ - HeroTeamLink(team_id=1, is_training=False, hero_id=1), + HeroTeamLink(team_id=1, is_training=False, hero_id=1), HeroTeamLink(team_id=1, is_training=True, hero_id=2) ] ``` @@ -317,26 +248,7 @@ So now we want to update the status of `is_training` to `False`. We can do that by iterating on the links: -```Python hl_lines="8-10" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial003.py[ln:88-89]!} - - # Code here omitted 👈 - -{!./docs_src/tutorial/many_to_many/tutorial003.py[ln:105-113]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/many_to_many/tutorial003.py!} -``` - -
+{* ./docs_src/tutorial/many_to_many/tutorial003_py310.py ln[82:83,99:107] hl[99:101] *} ## Run the Program with the Updated Relationships @@ -350,8 +262,8 @@ $ python app.py // Previous output omitted 🙈 // Automatically fetch team data on attribute access -INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters -FROM team +INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters +FROM team WHERE team.id = ? INFO Engine [generated in 0.00015s] (2,) @@ -366,16 +278,16 @@ INFO Engine COMMIT INFO Engine BEGIN (implicit) // Automatically fetch data on attribute access -INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age -FROM hero +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age +FROM hero WHERE hero.id = ? INFO Engine [cached since 0.2004s ago] (2,) -INFO Engine SELECT heroteamlink.team_id AS heroteamlink_team_id, heroteamlink.hero_id AS heroteamlink_hero_id, heroteamlink.is_training AS heroteamlink_is_training -FROM heroteamlink +INFO Engine SELECT heroteamlink.team_id AS heroteamlink_team_id, heroteamlink.hero_id AS heroteamlink_hero_id, heroteamlink.is_training AS heroteamlink_is_training +FROM heroteamlink WHERE ? = heroteamlink.hero_id INFO Engine [cached since 0.1005s ago] (2,) -INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters -FROM team +INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters +FROM team WHERE team.id = ? INFO Engine [cached since 0.09707s ago] (2,) @@ -383,13 +295,13 @@ INFO Engine [cached since 0.09707s ago] (2,) Spider-Boy team: headquarters='Sharp Tower' id=2 name='Preventers' is training: False // Automatically fetch data on attribute access -INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters -FROM team +INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters +FROM team WHERE team.id = ? INFO Engine [cached since 0.2097s ago] (1,) // Print Spider-Boy team, including link data, if is training -Spider-Boy team: headquarters='Sister Margaret’s Bar' id=1 name='Z-Force' is training: True +Spider-Boy team: headquarters='Sister Margaret's Bar' id=1 name='Z-Force' is training: True INFO Engine ROLLBACK ``` diff --git a/docs/tutorial/many-to-many/update-remove-relationships.md b/docs/tutorial/many-to-many/update-remove-relationships.md index 5cc55e1f41..ebc9ba3a85 100644 --- a/docs/tutorial/many-to-many/update-remove-relationships.md +++ b/docs/tutorial/many-to-many/update-remove-relationships.md @@ -4,14 +4,7 @@ Now we'll see how to update and remove these **many-to-many** relationships. We'll continue from where we left off with the previous code. -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/many_to_many/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/many_to_many/tutorial001_py310.py ln[0] *} ## Get Data to Update @@ -23,41 +16,11 @@ As you already know how these goes, I'll use the **short version** and get the d And because we are now using `select()`, we also have to import it. -```Python hl_lines="3 7-12" -{!./docs_src/tutorial/many_to_many/tutorial002.py[ln:1-3]!} - -# Some code here omitted 👈 - -{!./docs_src/tutorial/many_to_many/tutorial002.py[ln:78-83]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/many_to_many/tutorial002.py!} -``` - -
+{* ./docs_src/tutorial/many_to_many/tutorial002_py310.py ln[1,72:77] hl[1,72:77] *} And of course, we have to add `update_heroes()` to our `main()` function: -```Python hl_lines="6" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial002.py[ln:100-107]!} -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/many_to_many/tutorial002.py!} -``` - -
+{* ./docs_src/tutorial/many_to_many/tutorial002_py310.py ln[94:101] hl[97] *} ## Add Many-to-Many Relationships @@ -65,27 +28,15 @@ Now let's imagine that **Spider-Boy** thinks that the **Z-Force** team is super We can use the same **relationship attributes** to include `hero_spider_boy` in the `team_z_force.heroes`. -```Python hl_lines="10-12 14-15" -# Code above omitted 👆 +{* ./docs_src/tutorial/many_to_many/tutorial002_py310.py ln[72:84] hl[79:81,83:84] *} -{!./docs_src/tutorial/many_to_many/tutorial002.py[ln:78-90]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/many_to_many/tutorial002.py!} -``` +/// tip -
+Because we are accessing an attribute in the models right after we commit, with `hero_spider_boy.teams` and `team_z_force.heroes`, the data is refreshed automatically. -!!! tip - Because we are accessing an attribute in the models right after we commit, with `hero_spider_boy.teams` and `team_z_force.heroes`, the data is refreshed automatically. +So we don't have to call `session.refresh()`. - So we don't have to call `session.refresh()`. +/// We then commit the change, refresh, and print the updated **Spider-Boy**'s heroes to confirm. @@ -115,24 +66,24 @@ INFO Engine COMMIT INFO Engine BEGIN (implicit) // Automatically refresh the data while accessing the attribute .teams -INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age -FROM hero +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age +FROM hero WHERE hero.id = ? INFO Engine [generated in 0.00044s] (3,) -INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters -FROM team, heroteamlink +INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters +FROM team, heroteamlink WHERE ? = heroteamlink.hero_id AND team.id = heroteamlink.team_id INFO Engine [cached since 0.1648s ago] (3,) // Print Spider-Boy teams, including Z-Force 🎉 Updated Spider-Boy's Teams: [ Team(id=2, name='Preventers', headquarters='Sharp Tower'), - Team(id=1, name='Z-Force', headquarters='Sister Margaret’s Bar') + Team(id=1, name='Z-Force', headquarters='Sister Margaret's Bar') ] // Automatically refresh the data while accessing the attribute .heores -INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age -FROM hero, heroteamlink +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age +FROM hero, heroteamlink WHERE ? = heroteamlink.team_id AND hero.id = heroteamlink.hero_id INFO Engine [cached since 0.1499s ago] (1,) @@ -141,7 +92,7 @@ Z-Force heroes: [ Hero(name='Deadpond', age=None, id=1, secret_name='Dive Wilson'), Hero(name='Spider-Boy', age=None, id=3, secret_name='Pedro Parqueador', teams=[ Team(id=2, name='Preventers', headquarters='Sharp Tower'), - Team(id=1, name='Z-Force', headquarters='Sister Margaret’s Bar', heroes=[...]) + Team(id=1, name='Z-Force', headquarters='Sister Margaret's Bar', heroes=[...]) ]) ] ``` @@ -162,22 +113,7 @@ Because `hero_spider_boy.teams` is just a list (a special list managed by SQLAlc In this case, we use the method `.remove()`, that takes an item and removes it from the list. -```Python hl_lines="17-19 21-22" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial002.py[ln:78-97]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/many_to_many/tutorial002.py!} -``` - -
+{* ./docs_src/tutorial/many_to_many/tutorial002_py310.py ln[72:91] hl[86:88,90:91] *} And this time, just to show again that by using `back_populates` **SQLModel** (actually SQLAlchemy) takes care of connecting the models by their relationships, even though we performed the operation from the `hero_spider_boy` object (modifying `hero_spider_boy.teams`), we are adding `team_z_force` to the **session**. And we commit that, without even add `hero_spider_boy`. @@ -208,12 +144,12 @@ INFO Engine COMMIT INFO Engine BEGIN (implicit) // Automatically refresh the data while accessing the attribute .heroes -INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters -FROM team +INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters +FROM team WHERE team.id = ? INFO Engine [generated in 0.00029s] (1,) -INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age -FROM hero, heroteamlink +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age +FROM hero, heroteamlink WHERE ? = heroteamlink.team_id AND hero.id = heroteamlink.hero_id INFO Engine [cached since 0.5625s ago] (1,) @@ -223,12 +159,12 @@ Reverted Z-Force's heroes: [ ] // Automatically refresh the data while accessing the attribute .teams -INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age -FROM hero +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age +FROM hero WHERE hero.id = ? INFO Engine [cached since 0.4209s ago] (3,) -INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters -FROM team, heroteamlink +INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters +FROM team, heroteamlink WHERE ? = heroteamlink.hero_id AND team.id = heroteamlink.team_id INFO Engine [cached since 0.5842s ago] (3,) diff --git a/docs/tutorial/one.md b/docs/tutorial/one.md index 3b60653ed9..54cdbbea93 100644 --- a/docs/tutorial/one.md +++ b/docs/tutorial/one.md @@ -12,16 +12,9 @@ Let's see the utilities to read a single row. ## Continue From Previous Code -We'll continue with the same examples we have been using in the previous chapters to create and select data and we'll keep udpating them. +We'll continue with the same examples we have been using in the previous chapters to create and select data and we'll keep updating them. -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/indexes/tutorial002.py!} -``` - -
+{* ./docs_src/tutorial/indexes/tutorial002_py310.py ln[0] *} If you already executed the previous examples and have a database with data, **remove the database file** before running each example, that way you won't have duplicate data and you will be able to get the same results. @@ -29,50 +22,23 @@ If you already executed the previous examples and have a database with data, **r We have been iterating over the rows in a `result` object like: -```Python hl_lines="7-8" -# Code above omitted 👆 - -{!./docs_src/tutorial/indexes/tutorial002.py[ln:44-49]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/indexes/tutorial002.py!} -``` - -
+{* ./docs_src/tutorial/indexes/tutorial002_py310.py ln[42:47] hl[46:47] *} But let's say that we are not interested in all the rows, just the **first** one. We can call the `.first()` method on the `results` object to get the first row: -```Python hl_lines="7" -# Code above omitted 👆 - -{!./docs_src/tutorial/one/tutorial001.py[ln:44-49]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/one/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/one/tutorial001_py310.py ln[42:47] hl[46] *} This will return the first object in the `results` (if there was any). That way, we don't have to deal with an iterable or a list. -!!! tip - Notice that `.first()` is a method of the `results` object, not of the `select()` statement. +/// tip + +Notice that `.first()` is a method of the `results` object, not of the `select()` statement. + +/// Although this query would find two rows, by using `.first()` we get only the first row. @@ -86,8 +52,8 @@ $ python app.py // Some boilerplate output omitted 😉 // The SELECT with WHERE -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.age <= ? INFO Engine [no key 0.00021s] (35,) @@ -103,22 +69,7 @@ It would be possible that the SQL query doesn't find any row. In that case, `.first()` will return `None`: -```Python hl_lines="5 7" -# Code above omitted 👆 - -{!./docs_src/tutorial/one/tutorial002.py[ln:44-49]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/one/tutorial002.py!} -``` - -
+{* ./docs_src/tutorial/one/tutorial002_py310.py ln[42:47] hl[44,46] *} In this case, as there's no hero with an age less than 25, `.first()` will return `None`. @@ -132,8 +83,8 @@ $ python app.py // Some boilerplate output omitted 😉 // The SELECT with WHERE -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.age <= ? INFO Engine [no key 0.00021s] (35,) @@ -151,22 +102,7 @@ And if there was more than one, it would mean that there's an error in the syste In that case, instead of `.first()` we can use `.one()`: -```Python hl_lines="7" -# Code above omitted 👆 - -{!./docs_src/tutorial/one/tutorial003.py[ln:44-49]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/one/tutorial003.py!} -``` - -
+{* ./docs_src/tutorial/one/tutorial003_py310.py ln[42:47] hl[46] *} Here we know that there's only one `"Deadpond"`, and there shouldn't be any more than one. @@ -180,8 +116,8 @@ $ python app.py // Some boilerplate output omitted 😉 // The SELECT with WHERE -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.name = ? INFO Engine [no key 0.00015s] ('Deadpond',) @@ -203,8 +139,8 @@ $ python app.py // Some boilerplate output omitted 😉 // The SELECT with WHERE -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.name = ? INFO Engine [no key 0.00015s] ('Deadpond',) @@ -222,22 +158,7 @@ sqlalchemy.exc.MultipleResultsFound: Multiple rows were found when exactly one w Of course, even if we don't duplicate the data, we could get the same error if we send a query that finds more than one row and expect exactly one with `.one()`: -```Python hl_lines="5 7" -# Code above omitted 👆 - -{!./docs_src/tutorial/one/tutorial004.py[ln:44-49]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/one/tutorial004.py!} -``` - -
+{* ./docs_src/tutorial/one/tutorial004_py310.py ln[42:47] hl[44,46] *} That would find 2 rows, and would end up with the same error. @@ -245,22 +166,7 @@ That would find 2 rows, and would end up with the same error. And also, if we get no rows at all with `.one()`, it will also raise an error: -```Python hl_lines="5 7" -# Code above omitted 👆 - -{!./docs_src/tutorial/one/tutorial005.py[ln:44-49]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/one/tutorial005.py!} -``` - -
+{* ./docs_src/tutorial/one/tutorial005_py310.py ln[42:47] hl[44,46] *} In this case, as there are no heroes with an age less than 25, `.one()` will raise an error. @@ -274,8 +180,8 @@ $ python app.py // Some boilerplate output omitted 😉 // SELECT with WHERE -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.age < ? INFO Engine [no key 0.00014s] (25,) @@ -293,22 +199,7 @@ sqlalchemy.exc.NoResultFound: No row was found when one was required Of course, with `.first()` and `.one()` you would also probably write all that in a more compact form most of the time, all in a single line (or at least a single Python statement): -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/one/tutorial006.py[ln:44-47]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/one/tutorial006.py!} -``` - -
+{* ./docs_src/tutorial/one/tutorial006_py310.py ln[42:45] hl[44] *} That would result in the same as some examples above. @@ -318,22 +209,7 @@ In many cases you might want to select a single row by its Id column with the ** You could do it the same way we have been doing with a `.where()` and then getting the first item with `.first()`: -```Python hl_lines="5 7" -# Code above omitted 👆 - -{!./docs_src/tutorial/one/tutorial007.py[ln:44-49]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/one/tutorial007.py!} -``` - -
+{* ./docs_src/tutorial/one/tutorial007_py310.py ln[42:47] hl[44,46] *} That would work correctly, as expected. But there's a shorter version. 👇 @@ -341,22 +217,7 @@ That would work correctly, as expected. But there's a shorter version. 👇 As selecting a single row by its Id column with the **primary key** is a common operation, there's a shortcut for it: -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/one/tutorial008.py[ln:44-47]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/one/tutorial008.py!} -``` - -
+{* ./docs_src/tutorial/one/tutorial008_py310.py ln[42:45] hl[44] *} `session.get(Hero, 1)` is an equivalent to creating a `select()`, then filtering by Id using `.where()`, and then getting the first item with `.first()`. @@ -370,8 +231,8 @@ $ python app.py // Some boilerplate output omitted 😉 // SELECT with WHERE -INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age -FROM hero +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age +FROM hero WHERE hero.id = ? INFO Engine [generated in 0.00021s] (1,) @@ -385,22 +246,7 @@ Hero: secret_name='Dive Wilson' age=None id=1 name='Deadpond' `.get()` behaves similar to `.first()`, if there's no data it will simply return `None` (instead of raising an error): -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/one/tutorial009.py[ln:44-47]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/one/tutorial009.py!} -``` - -
+{* ./docs_src/tutorial/one/tutorial009_py310.py ln[42:45] hl[44] *} Running that will output: @@ -413,8 +259,8 @@ $ python app.py // SELECT with WHERE INFO Engine BEGIN (implicit) -INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age -FROM hero +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age +FROM hero WHERE hero.id = ? INFO Engine [generated in 0.00024s] (9001,) @@ -426,4 +272,4 @@ Hero: None ## Recap -As querying the SQL database for a single row is a common operation, you know have several tools to do it in a short and simple way. 🎉 +As querying the SQL database for a single row is a common operation, you now have several tools to do it in a short and simple way. 🎉 diff --git a/docs/tutorial/relationship-attributes/back-populates.md b/docs/tutorial/relationship-attributes/back-populates.md index 86a4c2a70a..9bdcbf0c28 100644 --- a/docs/tutorial/relationship-attributes/back-populates.md +++ b/docs/tutorial/relationship-attributes/back-populates.md @@ -10,7 +10,7 @@ So, what is that `back_populates` argument in each `Relationship()`? The value is a string with the name of the attribute in the **other** model class. - + That tells **SQLModel** that if something changes in this model, it should change that attribute in the other model, and it will work even before committing with the session (that would force a refresh of the data). @@ -20,20 +20,7 @@ Let's understand that better with an example. Let's see how that works by writing an **incomplete** version first, without `back_populates`: -```Python hl_lines="11 21" -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py[ln:1-21]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py ln[1:19] hl[9,19] *} ## Read Data Objects @@ -41,46 +28,19 @@ Now, we will get the **Spider-Boy** hero and, *independently*, the **Preventers* As you already know how this works, I won't separate that in a select `statement`, `results`, etc. Let's use the shorter form in a single call: -```Python hl_lines="5-7 9-11" -# Code above omitted 👆 +{* ./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py ln[103:111] hl[105:107,109:111] *} -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py[ln:105-113]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py!} -``` +/// tip -
+When writing your own code, this is probably the style you will use most often, as it's shorter, more convenient, and you still get all the power of autocompletion and inline errors. -!!! tip - When writing your own code, this is probably the style you will use most often, as it's shorter, more convenient, and you still get all the power of autocompletion and inline errors. +/// ## Print the Data Now, let's print the current **Spider-Boy**, the current **Preventers** team, and particularly, the current **Preventers** list of heroes: -```Python hl_lines="13-15" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py[ln:105-117]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py ln[103:115] hl[113:115] *} Up to this point, it's all good. 😊 @@ -102,33 +62,17 @@ Notice that we have **Spider-Boy** there. Now let's update **Spider-Boy**, removing him from the team by setting `hero_spider_boy.team = None` and then let's print this object again: -```Python hl_lines="8 12" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py[ln:105-106]!} - - # Code here omitted 👈 - -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py[ln:119-123]!} +{* ./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py ln[103:104,117:121] hl[117,121] *} -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py!} -``` +The first important thing is, we *haven't committed* the hero yet, so accessing the list of heroes would not trigger an automatic refresh. -
+But in our code, in this exact point in time, we already said that **Spider-Boy** is no longer part of the **Preventers**. 🔥 -The first important thing is, we *haven't commited* the hero yet, so accessing the list of heroes would not trigger an automatic refresh. +/// tip -But in our code, in this exact point in time, we already said that **Spider-Boy** is no longer part of the **Preventers**. 🔥 +We could revert that later by not committing the **session**, but that's not what we are interested in here. -!!! tip - We could revert that later by not committing the **session**, but that's not what we are interested in here. +/// Here, at this point in the code, in memory, the code expects **Preventers** to *not include* **Spider-Boy**. @@ -144,10 +88,10 @@ But now, what happens when we print the `preventers_team.heroes`? ``` hl_lines="3" Preventers Team Heroes again: [ - Hero(name='Rusty-Man', age=48, id=2, secret_name='Tommy Sharp', team_id=2), - Hero(name='Spider-Boy', age=None, id=3, secret_name='Pedro Parqueador', team_id=2, team=None), - Hero(name='Tarantula', age=32, id=6, secret_name='Natalia Roman-on', team_id=2), - Hero(name='Dr. Weird', age=36, id=7, secret_name='Steve Weird', team_id=2), + Hero(name='Rusty-Man', age=48, id=2, secret_name='Tommy Sharp', team_id=2), + Hero(name='Spider-Boy', age=None, id=3, secret_name='Pedro Parqueador', team_id=2, team=None), + Hero(name='Tarantula', age=32, id=6, secret_name='Natalia Roman-on', team_id=2), + Hero(name='Dr. Weird', age=36, id=7, secret_name='Steve Weird', team_id=2), Hero(name='Captain North America', age=93, id=8, secret_name='Esteban Rogelios', team_id=2) ] ``` @@ -158,39 +102,20 @@ Oh, no! 😱 **Spider-Boy** is still listed there! Now, if we commit it and print again: -```Python hl_lines="8-9 15" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py[ln:105-106]!} - - # Code here omitted 👈 - -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py[ln:125-132]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py ln[103:104,123:130] hl[123:124,130] *} When we access `preventers_team.heroes` after the `commit`, that triggers a refresh, so we get the latest list, without **Spider-Boy**, so that's fine again: ``` -INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age, hero.team_id AS hero_team_id -FROM hero +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age, hero.team_id AS hero_team_id +FROM hero WHERE ? = hero.team_id 2021-08-13 11:15:24,658 INFO sqlalchemy.engine.Engine [cached since 0.1924s ago] (2,) Preventers Team Heroes after commit: [ - Hero(name='Rusty-Man', age=48, id=2, secret_name='Tommy Sharp', team_id=2), - Hero(name='Tarantula', age=32, id=6, secret_name='Natalia Roman-on', team_id=2), - Hero(name='Dr. Weird', age=36, id=7, secret_name='Steve Weird', team_id=2), + Hero(name='Rusty-Man', age=48, id=2, secret_name='Tommy Sharp', team_id=2), + Hero(name='Tarantula', age=32, id=6, secret_name='Natalia Roman-on', team_id=2), + Hero(name='Dr. Weird', age=36, id=7, secret_name='Steve Weird', team_id=2), Hero(name='Captain North America', age=93, id=8, secret_name='Esteban Rogelios', team_id=2) ] ``` @@ -209,48 +134,19 @@ That's what `back_populates` is for. ✨ Let's add it back: -```Python hl_lines="11 21" -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py[ln:1-21]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py!} -``` - -
+{* ./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py ln[1:19] hl[9,19] *} And we can keep the rest of the code the same: -```Python hl_lines="8 12" -# Code above omitted 👆 +{* ./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py ln[103:104,117:121] hl[117,121] *} -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py[ln:105-106]!} +/// tip - # Code here omitted 👈 +This is the same section where we updated `hero_spider_boy.team` to `None` but we *haven't committed* that change yet. -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py[ln:119-123]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py!} -``` +The same section that caused a problem before. -
- -!!! tip - This is the same section where we updated `hero_spider_boy.team` to `None` but we *haven't committed* that change yet. - - The same section that caused a problem before. +/// ## Review the Result @@ -260,9 +156,9 @@ That second print would output: ``` Preventers Team Heroes again: [ - Hero(name='Rusty-Man', age=48, id=2, secret_name='Tommy Sharp', team_id=2), - Hero(name='Tarantula', age=32, id=6, secret_name='Natalia Roman-on', team_id=2), - Hero(name='Dr. Weird', age=36, id=7, secret_name='Steve Weird', team_id=2), + Hero(name='Rusty-Man', age=48, id=2, secret_name='Tommy Sharp', team_id=2), + Hero(name='Tarantula', age=32, id=6, secret_name='Natalia Roman-on', team_id=2), + Hero(name='Dr. Weird', age=36, id=7, secret_name='Steve Weird', team_id=2), Hero(name='Captain North America', age=93, id=8, secret_name='Esteban Rogelios', team_id=2) ] ``` @@ -275,43 +171,15 @@ Now that you know why `back_populates` is there, let's review the exact value ag It's quite simple code, it's just a string, but it might be confusing to think exactly *what* string should go there: -```Python hl_lines="11 21" -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py[ln:1-21]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py!} -``` - -
+{* ./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py ln[1:19] hl[9,19] *} The string in `back_populates` is the name of the attribute *in the other* model, that will reference *the current* model. - + So, in the class `Team`, we have an attribute `heroes` and we declare it with `Relationship(back_populates="team")`. -```Python hl_lines="8" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py[ln:6-11]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py!} -``` - -
+{* ./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py ln[4:9] hl[9] *} The string in `back_populates="team"` refers to the attribute `team` in the class `Hero` (the other class). @@ -319,25 +187,13 @@ And, in the class `Hero`, we declare an attribute `team`, and we declare it with So, the string `"heroes"` refers to the attribute `heroes` in the class `Team`. -```Python hl_lines="10" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py[ln:14-21]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview +{* ./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py ln[12:19] hl[19] *} -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py!} -``` +/// tip -
+Each **relationship attribute** points to the other one, in the other model, using `back_populates`. -!!! tip - Each **relationship attribute** points to the other one, in the other model, using `back_populates`. +/// Although it's simple code, it can be confusing to think about 😵, because the same line has concepts related to both models in multiple places: @@ -354,21 +210,6 @@ So, if you are in the class `Hero`, the value of `back_populates` for any relati So, `back_populates` would most probably be something like `"hero"` or `"heroes"`. - - -```Python hl_lines="3 10 13 15" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial003.py[ln:29-41]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial003.py!} -``` + -
+{* ./docs_src/tutorial/relationship_attributes/back_populates/tutorial003_py310.py ln[27:39] hl[27,34,37,39] *} diff --git a/docs/tutorial/relationship-attributes/cascade-delete-relationships.md b/docs/tutorial/relationship-attributes/cascade-delete-relationships.md new file mode 100644 index 0000000000..3b6fe9156c --- /dev/null +++ b/docs/tutorial/relationship-attributes/cascade-delete-relationships.md @@ -0,0 +1,651 @@ +# Cascade Delete Relationships + +What happens if we **delete** a team that has a **relationship** with heroes? + +Should those heroes be **automatically deleted** too? That's called a "**cascade**", because the initial deletion causes a cascade of other deletions. + +Should their `team_id` instead be set to `NULL` in the database? + +Let's see how to configure that with **SQLModel**. + +/// info + +This feature, including `cascade_delete`, `ondelete`, and `passive_deletes`, is available since SQLModel version `0.0.21`. + +/// + +## Initial Heroes and Teams + +Let's say that we have these **teams** and **heroes**. + +### Team Table + +| id | name | headquarters | +| ---- | ---------- | --------------------- | +| 1 | Z-Force | Sister Margaret's Bar | +| 2 | Preventers | Sharp Tower | +| 3 | Wakaland | Wakaland Capital City | + +### Hero Table + +| id | name | secret_name | age | team_id | +| ---- | --------------- | ---------------- | ---- | ------- | +| 1 | Deadpond | Dive WIlson | | 1 | +| 2 | Rusty-Man | Tommy Sharp | 48 | 2 | +| 3 | Spider-Boy | Pedro Parqueador | | 2 | +| 4 | Black Lion | Trevor Challa | 35 | 3 | +| 5 | Princess Sure-E | Sure-E | | 3 | + +### Visual Teams and Heroes + +We could visualize them like this: + +```mermaid +flowchart TB + subgraph "Z-Force" + d("Deadpond") + end + subgraph "Preventers" + r("Rusty-Man") + s("Spider-Boy") + end + subgraph "Wakaland" + b("Black Lion") + p("Princess Sure-E") + end +``` + +## Delete a Team with Heroes + +When we **delete a team**, we have to do something with the associated heroes. + +By default, their foreign key pointing to the team will be set to `NULL` in the database. + +But let's say we want the associated heroes to be **automatically deleted**. + +For example, we could delete the team `Wakaland`: + +```mermaid +flowchart TB + subgraph zforce["Z-Force"] + d("Deadpond") + end + subgraph preventers["Preventers"] + r("Rusty-Man") + s("Spider-Boy") + end + subgraph wakaland["Wakaland"] + b("Black Lion") + p("Princess Sure-E") + end + style wakaland fill:#fee,stroke:#900 +``` + +And we would want the heroes `Black Lion` and `Princess Sure-E` to be **automatically deleted** too. + +So we would end up with these teams and heroes: + +```mermaid +flowchart TB + subgraph zforce["Team Z-Force"] + d("Deadpond") + end + subgraph preventers["Team Preventers"] + r("Rusty-Man") + s("Spider-Boy") + end +``` + +## Configure Automatic Deletion + +There are **two places** where this automatic deletion is configured: + +* in **Python code** +* in the **database** + +## Delete in Python with `cascade_delete` + +When creating a `Relationship()`, we can set `cascade_delete=True`. + +This configures SQLModel to **automatically delete** the related records (heroes) **when the initial one is deleted** (a team). + +{* ./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py310.py ln[1:9] hl[9] *} + +With this configuration, when we delete a team, SQLModel (actually SQLAlchemy) will: + +* Make sure the objects for the **related records are loaded**, in this case, the `heroes`. If they are not loaded, it will send a `SELECT` query to the database to get them. +* Send a `DELETE` query to the database **including each related record** (each hero). +* Finally, **delete the initial record** (the team) with another `DELETE` query. + +This way, the internal **Python code** will take care of deleting the related records, by emitting the necessary SQL queries for each of them. + +/// tip + +The `cascade_delete` parameter is set in the `Relationship()`, on the model that **doesn't have a foreign key**. + +/// + +/// note | Technical Details + +Setting `cascade_delete=True` in the `Relationship()` will configure SQLAlchemy to use `cascade="all, delete-orphan"`, which is the most common and useful configuration when wanting to cascade deletes. + +You can read more about it in the SQLAlchemy docs. + +/// + +## Delete in the Database with `ondelete` + +In the previous section we saw that using `cascade_delete` handles automatic deletions from the Python code. + +But what happens if someone **interacts with the database directly**, not using our code, and **deletes a team with SQL**? + +For those cases, we can configure the database to **automatically delete** the related records with the `ondelete` parameter in `Field()`. + +### `ondelete` Options + +The `ondelete` parameter will set a SQL `ON DELETE` in the **foreign key column** in the database. + +`ondelete` can have these values: + +* `CASCADE`: **Automatically delete this record** (hero) when the related one (team) is deleted. +* `SET NULL`: Set this **foreign key** (`hero.team_id`) field to `NULL` when the related record is deleted. +* `RESTRICT`: **Prevent** the deletion of this record (hero) if there is a foreign key value by raising an error. + +## Set `ondelete` to `CASCADE` + +If we want to configure the database to **automatically delete** the related records when the parent is deleted, we can set `ondelete="CASCADE"`. + +{* ./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py310.py ln[1:19] hl[18] *} + +Now, when we **create the tables** in the database, the `team_id` column in the `Hero` table will have an `ON DELETE CASCADE` in its definition at the database level. + +This will **configure the database** to **automatically delete** the records (heroes) when the related record (team) is deleted. + +/// tip + +The `ondelete` parameter is set in the `Field()`, on the model that **has a foreign key**. + +/// + +## Using `cascade_delete` or `ondelete` + +At this point, you might be wondering if you should use `cascade_delete` or `ondelete`. The answer is: **both**! 🤓 + +The `ondelete` will **configure the database**, in case someone interacts with it directly. + +But `cascade_delete` is still needed to tell SQLAlchemy that it should delete the **Python objects** in memory. + +### Foreign Key Constraint Support + +Some databases don't support foreign key constraints. + +For example, **SQLite** doesn't support them by default. They have to be manually enabled with a custom SQL command: + +``` +PRAGMA foreign_keys = ON; +``` + +So, in general it's a good idea to have both `cascade_delete` and `ondelete` configured. + +/// tip + +You will learn more about how to **disable the default** automatic SQLModel (SQLAlchemy) behavior and **only rely on the database** down below, in the section about `passive_deletes`. + +/// + +### `cascade_delete` on `Relationship()` and `ondelete` on `Field()` + +Just a note to remember... 🤓 + +* `ondelete` is put on the `Field()` with a **foreign key**. On the **"many"** side in "one-to-many" relationships. + +```Python +class Hero(SQLModel, table=True): + ... + + team_id: int = Field(foreign_key="team.id", ondelete="CASCADE") +``` + +* `cascade_delete` is put on the `Relationship()`. Normally on the **"one"** side in "one-to-many" relationships, the side **without a foreign key**. + +```Python +class Team(SQLModel, table=True): + ... + + heroes: list[Hero] = Relationship(cascade_delete=True) +``` + +## Remove a Team and its Heroes + +Now, when we **delete a team**, we don't need to do anything else, it's **automatically** going to **delete its heroes**. + +{* ./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py310.py ln[76:82] hl[80] *} + +## Confirm Heroes are Deleted + +We can confirm that **after deleting the team** `Wakaland`, the heroes `Black Lion` and `Princess Sure-E` are **also deleted**. + +If we try to select them from the database, we will **no longer find them**. + +{* ./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py310.py ln[85:95] hl[87,90,92,95] *} + +## Run the Program with `cascade_delete=True` and `ondelete="CASCADE"` + +We can confirm everything is working by running the program. + +
+ +```console +$ python app.py + +// Some boilerplate and previous output omitted 😉 + +// The team table is created as before +CREATE TABLE team ( + id INTEGER NOT NULL, + name VARCHAR NOT NULL, + headquarters VARCHAR NOT NULL, + PRIMARY KEY (id) +) + +// The hero table is created with the ON DELETE CASCADE 🎉 +// In SQLite, it also includes REFERENCES team (id), this is needed by SQLite to work with the ON DELETE CASCADE properly. +// SQLAlchemy takes care of setting it up for us to make sure it works 🤓 +CREATE TABLE hero ( + id INTEGER NOT NULL, + name VARCHAR NOT NULL, + secret_name VARCHAR NOT NULL, + age INTEGER, + team_id INTEGER, + PRIMARY KEY (id), + FOREIGN KEY(team_id) REFERENCES team (id) ON DELETE CASCADE +) + +// We select the team Wakaland +INFO Engine SELECT team.id, team.name, team.headquarters +FROM team +WHERE team.name = ? +INFO Engine [generated in 0.00014s] ('Wakaland',) + +// Then, because of cascade_delete, right before deleting Wakaland, SQLAlchemy loads the heroes +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age, hero.team_id AS hero_team_id +FROM hero +WHERE ? = hero.team_id +INFO Engine [generated in 0.00020s] (3,) + +// Next, before deleting the Wakaland team, it sends a DELETE statement including each related hero: Black Lion and Princess Sure-E, with IDs 4 and 5 +INFO Engine DELETE FROM hero WHERE hero.id = ? +INFO Engine [generated in 0.00022s] [(4,), (5,)] + +// After that, it will send the delete for the team Wakaland with ID 3 +INFO Engine DELETE FROM team WHERE team.id = ? +INFO Engine [generated in 0.00017s] (3,) + +// Print the deleted team +Deleted team: name='Wakaland' id=3 headquarters='Wakaland Capital City' + +// Finally, we try to select the heroes from Wakaland, Black Lion and Princess Sure-E and print them, but they are now deleted +Black Lion not found: None +Princess Sure-E not found: None +``` + +
+ +## `ondelete` with `SET NULL` + +We can configure the database to **set the foreign key** (the `team_id` in the `hero` table) to **`NULL`** when the related record (in the `team` table) is deleted. + +In this case, the side with `Relationship()` won't have `cascade_delete`, but the side with `Field()` and a `foreign_key` will have `ondelete="SET NULL"`. + +{* ./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002_py310.py ln[1:21] hl[19] *} + +The configuration above is setting the `team_id` column from the `Hero` table to have an `ON DELETE SET NULL`. + +This way, when someone deletes a team from the database using SQL directly, the database will go to the heroes for that team and set `team_id` to `NULL` (if the database supports it). + +/// tip + +The foreign key should allow `None` values (`NULL` in the database), otherwise you would end up having an Integrity Error by violating the `NOT NULL` constraint. + +So `team_id` needs to have a type with `None`, like: + +```Python +team_id: int | None +``` + +/// + +### Not Using `ondelete="SET NULL"` + +What happens if you don't use `ondelete="SET NULL"`, don't set anything on `cascade_delete`, and delete a team? + +The default behavior is that SQLModel (actually SQLAlchemy) will go to the heroes and set their `team_id` to `NULL` from the **Python code**. + +So, **by default**, those `team_id` fields will be **set to `NULL`**. + +But if someone goes to the database and **manually deletes a team**, the heroes could end up with a `team_id` pointing to a non-existing team. + +Adding the `ondelete="SET NULL"` configures the database itself to also set those fields to `NULL`. + +But if you delete a team from code, by default, SQLModel (actually SQLAlchemy) will update those `team_id` fields to `NULL` even before the database `SET NULL` takes effect. + +### Removing a Team with `SET NULL` + +Removing a team has the **same code** as before, the only thing that changes is the configuration underneath in the database. + +{* ./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002_py310.py ln[78:84] hl[82] *} + +The result would be these tables. + +#### Team Table after `SET NULL` + +| id | name | headquarters | +| ---- | ---------- | --------------------- | +| 1 | Z-Force | Sister Margaret's Bar | +| 2 | Preventers | Sharp Tower | + +#### Hero Table after `SET NULL` + +| id | name | secret_name | age | team_id | +| ---- | --------------- | ---------------- | ---- | ------- | +| 1 | Deadpond | Dive Wilson | | 1 | +| 2 | Rusty-Man | Tommy Sharp | 48 | 2 | +| 3 | Spider-Boy | Pedro Parqueador | | 2 | +| 4 | Black Lion | Trevor Challa | 35 | NULL | +| 5 | Princess Sure-E | Sure-E | | NULL | + +#### Visual Teams and Heroes after `SET NULL` + +We could visualize them like this: + +```mermaid +flowchart TB + subgraph "Z-Force" + d("Deadpond") + end + subgraph "Preventers" + r("Rusty-Man") + s("Spider-Boy") + end + b("Black Lion") + p("Princess Sure-E") +``` + +### Run the program with `SET NULL` + +Let's confirm it all works by running the program now: + +
+ +```console +$ python app.py + +// Some boilerplate and previous output omitted 😉 + +// The hero table is created with the ON DELETE SET NULL 🎉 +// In SQLite, it also includes: REFERENCES team (id). This REFERENCES is needed by SQLite to work with the ON DELETE CASCADE properly. +// SQLModel with SQLAlchemy takes care of setting it up for us to make sure it works 🤓 +CREATE TABLE hero ( + id INTEGER NOT NULL, + name VARCHAR NOT NULL, + secret_name VARCHAR NOT NULL, + age INTEGER, + team_id INTEGER, + PRIMARY KEY (id), + FOREIGN KEY(team_id) REFERENCES team (id) ON DELETE SET NULL +) + +// We select the team Wakaland +INFO Engine SELECT team.id, team.name, team.headquarters +FROM team +WHERE team.id = ? +INFO Engine [generated in 0.00010s] (3,) +Team Wakaland: id=3 name='Wakaland' headquarters='Wakaland Capital City' + +// Then, right before deleting Wakaland, the heroes are loaded automatically +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age, hero.team_id AS hero_team_id +FROM hero +WHERE ? = hero.team_id +INFO Engine [generated in 0.00020s] (3,) + +// Next, before deleting the Wakaland team, it sends an UPDATE statement including each related hero: Black Lion and Princess Sure-E, with IDs 4 and 5, to set their team_id to NULL. This is not the SET NULL we added, this is just the default SQLModel (SQLAlchemy) behavior. +INFO Engine UPDATE hero SET team_id=? WHERE hero.id = ? +INFO Engine [generated in 0.00009s] [(None, 4), (None, 5)] + +// After that, it will send the delete for the team Wakaland with ID 3 +INFO Engine DELETE FROM team WHERE team.id = ? +INFO Engine [generated in 0.00017s] (3,) + +// Print the deleted team +Deleted team: name='Wakaland' id=3 headquarters='Wakaland Capital City' + +// Finally, we select the heroes Black Lion and Princess Sure-E and print them, they no longer have a team +Black Lion has no team: age=35 id=4 name='Black Lion' secret_name='Trevor Challa' team_id=None +Princess Sure-E has no team: age=None id=5 name='Princess Sure-E' secret_name='Sure-E' team_id=None +``` + +
+ +The team `Wakaland` was deleted and all of its heroes were left without a team, or in other words, with their `team_id` set to `NULL`, but still kept in the database! 🤓 + +## Let the Database Handle it with `passive_deletes` + +In the previous examples we configured `ondelete` with `CASCADE` and `SET NULL` to configure the database to handle the deletion of related records automatically. But we actually **never used that functionality** ourselves, because SQLModel (SQLAlchemy) **by default loads** the related records and **deletes** them or updates them with **NULL** before sending the `DELETE` for the team. + +If you know your database would be able to correctly handle the deletes or updates on its own, just with `ondelete="CASCADE"` or `ondelete="SET NULL"`, you can use `passive_deletes="all"` in the `Relationship()` to tell SQLModel (actually SQLAlchemy) to **not delete or update** those records (for heroes) before sending the `DELETE` for the team. + +### Enable Foreign Key Support in SQLite + +To be able to test this out with SQLite, we first need to enable foreign key support. + +{* ./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003_py310.py ln[30:33] hl[33] *} + +/// info + +You can learn more about SQLite, foreign keys, and this SQL command on the SQLAlchemy docs. + +/// + +### Use `passive_deletes="all"` + +Now let's update the table model for `Team` to use `passive_deletes="all"` in the `Relationship()` for heroes. + +We will also use `ondelete="SET NULL"` in the `Hero` model table, in the foreign key `Field()` for the `team_id` to make the database set those fields to `NULL` automatically. + +{* ./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003_py310.py ln[1:21] hl[9,19] *} + +### Run the Program with `passive_deletes` + +Now, if we run the program, we will see that SQLModel (SQLAlchemy) is no longer loading and updating the heroes, it just sends the `DELETE` for the team. + +
+ +```console +$ python app.py + +// Some boilerplate and previous output omitted 😉 + +// The hero table is created with the ON DELETE SET NULL as before +CREATE TABLE hero ( + id INTEGER NOT NULL, + name VARCHAR NOT NULL, + secret_name VARCHAR NOT NULL, + age INTEGER, + team_id INTEGER, + PRIMARY KEY (id), + FOREIGN KEY(team_id) REFERENCES team (id) ON DELETE SET NULL +) + +// For SQLite, we also send the custom command to enable foreign key support +INFO Engine PRAGMA foreign_keys=ON + +// We select and print the team Wakaland +Team Wakaland: id=3 name='Wakaland' headquarters='Wakaland Capital City' + +// We won't see another SELECT for the heroes, nor an UPDATE or DELETE. SQLModel (with SQLAlchemy) won't try to load and update (or delete) the related records for heroes, it will just send the DELETE for the team right away. +INFO Engine DELETE FROM team WHERE team.id = ? +INFO Engine [generated in 0.00013s] (3,) + +// At this point, because we enabled foreign key support for SQLite, the database will take care of updating the records for heroes automatically, setting their team_id to NULL + +// Print the deleted team +Deleted team: name='Wakaland' id=3 headquarters='Wakaland Capital City' + +// Finally, we select the heroes Black Lion and Princess Sure-E and print them, they no longer have a team +Black Lion has no team: age=35 id=4 name='Black Lion' secret_name='Trevor Challa' team_id=None +Princess Sure-E has no team: age=None id=5 name='Princess Sure-E' secret_name='Sure-E' team_id=None +``` + +
+ +## `ondelete` with `RESTRICT` + +We can also configure the database to **prevent the deletion** of a record (a team) if there are related records (heroes). + +In this case, when someone attempts to **delete a team with heroes** in it, the database will **raise an error**. + +And because this is configured in the database, it will happen even if someone **interacts with the database directly using SQL** (if the database supports it). + +/// tip + +For SQLite, this also needs enabling foreign key support. + +/// + +### Enable Foreign Key Support in SQLite for `RESTRICT` + +As `ondelete="RESTRICT"` is mainly a database-level constraint, let's enable foreign key support in SQLite first to be able to test it. + +{* ./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004_py310.py ln[30:33] hl[33] *} + +### Use `ondelete="RESTRICT"` + +Let's set `ondelete="RESTRICT"` in the foreign key `Field()` for the `team_id` in the `Hero` model table. + +And in the `Team` model table, we will use `passive_deletes="all"` in the `Relationship()` for heroes, this way the default behavior of setting foreign keys from deleted models to `NULL` will be disabled, and when we try to delete a team with heroes, the database will **raise an error**. + +/// tip + +Notice that we don't set `cascade_delete` in the `Team` model table. + +/// + +{* ./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004_py310.py ln[1:21] hl[9,19] *} + +### Run the Program with `RESTRICT`, See the Error + +Now, if we run the program and try to delete a team with heroes, we will see an error. + +
+ +```console +$ python app.py + +// Some boilerplate and previous output omitted 😉 + +// The hero table is created with the ON DELETE RESTRICT +CREATE TABLE hero ( + id INTEGER NOT NULL, + name VARCHAR NOT NULL, + secret_name VARCHAR NOT NULL, + age INTEGER, + team_id INTEGER, + PRIMARY KEY (id), + FOREIGN KEY(team_id) REFERENCES team (id) ON DELETE RESTRICT +) + +// Now, when we reach the point of deleting a team with heroes, we will see an error +Traceback (most recent call last): + File "/home/user/code... + +sqlite3.IntegrityError: FOREIGN KEY constraint failed + +// More error output here... + +sqlalchemy.exc.IntegrityError: (sqlite3.IntegrityError) FOREIGN KEY constraint failed +[SQL: DELETE FROM team WHERE team.id = ?] +[parameters: (3,)] +``` + +
+ +Great! The database didn't let us commit the mistake of deleting a team with heroes. 🤓 + +/// tip + +If you want to test if the `PRAGMA foreign_keys=ON` is necessary, **comment that line** and run it again, you will **not see an error**. 😱 + +The same with `passive_deletes="all"`, if you **comment that line**, SQLModel (SQLAlchemy) will load and update the heroes before deleting the team, set their foreign key `team_id` to `NULL` and **the constraint won't work as expected**, you will not see an error. 😅 + +/// + +### Update Heroes Before Deleting the Team + +After having the `ondelete="RESTRICT"` in place, SQLite configured to support foreign keys, and `passive_deletes="all"` in the `Relationship()`, if we try to delete a team with heroes, we will see an error. + +If we want to delete the team, we need to **update the heroes first** and set their `team_id` to `None` (or `NULL` in the database). + +By calling the method `.clear()` from a list, we remove all its items. So, by calling `team.heroes.clear()` and saving that to the database, we disassociate the heroes from the team, that will set their `team_id` to `None`. + +/// tip + +Calling `team.heroes.clear()` is very similar to what SQLModel (actually SQLAlchemy) would have done if we didn't have `passive_deletes="all"` configured. + +/// + +{* ./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial005_py310.py ln[80:88] hl[84] *} + +### Run the Program Deleting Heroes First + +Now, if we run the program and delete the heroes first, we will be able to delete the team without any issues. + +
+ +```console +$ python app.py + +// Some boilerplate and previous output omitted 😉 + +// The hero table is created with the ON DELETE RESTRICT +CREATE TABLE hero ( + id INTEGER NOT NULL, + name VARCHAR NOT NULL, + secret_name VARCHAR NOT NULL, + age INTEGER, + team_id INTEGER, + PRIMARY KEY (id), + FOREIGN KEY(team_id) REFERENCES team (id) ON DELETE RESTRICT +) + +// We manually disassociate the heroes from the team +INFO Engine UPDATE hero SET team_id=? WHERE hero.id = ? +INFO Engine [generated in 0.00008s] [(None, 4), (None, 5)] + +// We print the team from which we removed heroes +Team with removed heroes: name='Wakaland' id=3 headquarters='Wakaland Capital City' + +// Now we can delete the team +INFO Engine DELETE FROM team WHERE team.id = ? +INFO Engine [generated in 0.00008s] (3,) +INFO Engine COMMIT +Deleted team: name='Wakaland' id=3 headquarters='Wakaland Capital City' + +// The heroes Black Lion and Princess Sure-E are no longer associated with the team +Black Lion has no team: secret_name='Trevor Challa' name='Black Lion' team_id=None age=35 id=4 +Princess Sure-E has no team: secret_name='Sure-E' name='Princess Sure-E' team_id=None age=None id=5 +``` + +
+ +## Conclusion + +In many cases, **you don't really need to configure anything**. 😎 + +In some cases, when you want to **cascade** the delete of a record to its related records automatically (delete a team with its heroes), you can: + +* Use `cascade_delete=True` in the `Relationship()` on the side **without a foreign key** +* And use `ondelete="CASCADE"` in the `Field()` with the **foreign key** + +That will **cover most of the use cases**. 🚀 + +And if you need something else, you can refer the additional options described above. 🤓 diff --git a/docs/tutorial/relationship-attributes/create-and-update-relationships.md b/docs/tutorial/relationship-attributes/create-and-update-relationships.md index dc05af6eb6..d0de56735d 100644 --- a/docs/tutorial/relationship-attributes/create-and-update-relationships.md +++ b/docs/tutorial/relationship-attributes/create-and-update-relationships.md @@ -6,22 +6,7 @@ Let's see now how to create data with relationships using these new **relationsh Let's check the old code we used to create some heroes and teams: -```Python hl_lines="9 12 18 24" -# Code above omitted 👆 - -{!./docs_src/tutorial/connect/insert/tutorial001.py[ln:31-60]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/connect/insert/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/connect/insert/tutorial001_py310.py ln[29:58] hl[35,38,44,50] *} There are several things to **notice** here. @@ -41,26 +26,11 @@ This is the first area where these **relationship attributes** can help. 🤓 Now let's do all that, but this time using the new, shiny `Relationship` attributes: -```Python hl_lines="9 12 18" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py[ln:34-57]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py310.py ln[32:55] hl[38,41,47] *} Now we can create the `Team` instances and pass them directly to the new `team` argument when creating the `Hero` instances, as `team=team_preventers` instead of `team_id=team_preventers.id`. -And thanks to SQLAlchemy and how it works underneath, these teams don't even have to have an ID yet, but because we are assigning the whole object to each hero, those teams **will be automatically created** in the database, the automatic ID will be generated, and will be set in the `team_id` column for each of the corresponding hero rows. +And thanks to SQLAlchemy and how it works underneath, these teams don't even need to have an ID yet, but because we are assigning the whole object to each hero, those teams **will be automatically created** in the database, the automatic ID will be generated, and will be set in the `team_id` column for each of the corresponding hero rows. In fact, now we don't even have to put the teams explicitly in the session with `session.add(team)`, because these `Team` instances are **already associated** with heroes that **we do** `add` to the session. @@ -72,26 +42,7 @@ And then, as you can see, we only have to do one `commit()`. The same way we could assign an integer with a `team.id` to a `hero.team_id`, we can also assign the `Team` instance to the `hero.team`: -```Python hl_lines="8" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py[ln:34-35]!} - - # Previous code here omitted 👈 - -{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py[ln:59-63]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py310.py ln[32:33,57:61] hl[57] *} ## Create a Team with Heroes @@ -99,26 +50,7 @@ Before, we created some `Team` instances and passed them in the `team=` argument We could also create the `Hero` instances first, and then pass them in the `heroes=` argument that takes a list, when creating a `Team` instance: -```Python hl_lines="13 15-16" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py[ln:34-35]!} - - # Previous code here omitted 👈 - -{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py[ln:65-75]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py310.py ln[32:33,63:73] hl[68,70:71] *} Here we create two heroes first, **Black Lion** and **Princess Sure-E**, and then we pass them in the `heroes` argument. @@ -134,26 +66,7 @@ As the attribute `team.heroes` behaves like a list, we can simply append to it. Let's create some more heroes and add them to the `team_preventers.heroes` list attribute: -```Python hl_lines="14-18" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py[ln:34-35]!} - - # Previous code here omitted 👈 - -{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py[ln:77-93]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py310.py ln[32:33,75:91] hl[81:85] *} The attribute `team_preventers.heroes` behaves like a list. But it's a special type of list, because when we modify it adding heroes to it, **SQLModel** (actually SQLAlchemy) **keeps track of the necessary changes** to be done in the database. diff --git a/docs/tutorial/relationship-attributes/define-relationships-attributes.md b/docs/tutorial/relationship-attributes/define-relationships-attributes.md index 09d7b2765b..c5307d3663 100644 --- a/docs/tutorial/relationship-attributes/define-relationships-attributes.md +++ b/docs/tutorial/relationship-attributes/define-relationships-attributes.md @@ -14,7 +14,7 @@ We currently have a `team` table: 1PreventersSharp Tower -2Z-ForceSister Margaret’s Bar +2Z-ForceSister Margaret's Bar @@ -41,20 +41,7 @@ Now that you know how these tables work underneath and how the model classes rep Up to now, we have only used the `team_id` column to connect the tables when querying with `select()`: -```Python hl_lines="18" -{!./docs_src/tutorial/connect/insert/tutorial001.py[ln:1-18]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/connect/insert/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/connect/insert/tutorial001_py310.py ln[1:16] hl[16] *} This is a **plain field** like all the others, all representing a **column in the table**. @@ -62,41 +49,15 @@ But now let's add a couple of new special attributes to these model classes, let First, import `Relationship` from `sqlmodel`: -```Python hl_lines="3" -{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py[ln:1-3]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py310.py ln[1] hl[1] *} Next, use that `Relationship` to declare a new attribute in the model classes: -```Python hl_lines="11 21" -{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py[ln:1-21]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py310.py ln[1:19] hl[9,19] *} ## What Are These Relationship Attributes -This new attributes are not the same as fields, they **don't represent a column** directly in the database, and their value is not a singular value like an integer. Their value is the actual **entire object** that is related. +These new attributes are not the same as fields, they **don't represent a column** directly in the database, and their value is not a singular value like an integer. Their value is the actual **entire object** that is related. So, in the case of a `Hero` instance, if you call `hero.team`, you will get the entire `Team` instance object that this hero belongs to. ✨ @@ -107,17 +68,15 @@ if hero.team: print(hero.team.name) ``` -## Optional Relationship Attributes +## Relationship Attributes or `None` -Notice that in the `Hero` class, the type annotation for `team` is `Optional[Team]`. +Notice that in the `Hero` class, the type annotation for `team` is `Team | None`. This means that this attribute could be `None`, or it could be a full `Team` object. This is because the related **`team_id` could also be `None`** (or `NULL` in the database). -If it was required for a `Hero` instance to belong to a `Team`, then the `team_id` would be `int` instead of `Optional[int]`. - -And the `team` attribute would be a `Team` instead of `Optional[Team]`. +If it was required for a `Hero` instance to belong to a `Team`, then the `team_id` would be `int` instead of `int | None`, its `Field` would be `Field(foreign_key="team.id")` instead of `Field(default=None, foreign_key="team.id")` and the `team` attribute would be a `Team` instead of `Team | None`. ## Relationship Attributes With Lists @@ -125,10 +84,13 @@ And in the `Team` class, the `heroes` attribute is annotated as a list of `Hero` **SQLModel** (actually SQLAlchemy) is smart enough to know that the relationship is established by the `team_id`, as that's the foreign key that points from the `hero` table to the `team` table, so we don't have to specify that explicitly here. -!!! tip - There's a couple of things we'll check again in some of the next chapters, about the `List["Hero"]` and the `back_populates`. +/// tip + +There's a couple of things we'll check again in some of the next chapters, about the `list["Hero"]` and the `back_populates`. + +But for now, let's first see how to use these relationship attributes. - But for now, let's first see how to use these relationship attributes. +/// ## Next Steps diff --git a/docs/tutorial/relationship-attributes/index.md b/docs/tutorial/relationship-attributes/index.md index 2b8b843bbd..f63b2669e3 100644 --- a/docs/tutorial/relationship-attributes/index.md +++ b/docs/tutorial/relationship-attributes/index.md @@ -4,11 +4,14 @@ In the previous chapters we discussed how to manage databases with tables that h And then we read the data together with `select()` and using `.where()` or `.join()` to connect it. -Now we will see how to use **Relationship Attributes**, an extra feature of **SQLModel** (and SQLAlchemy) to work with the data in the database in way much more familiar way, and closer to normal Python code. +Now we will see how to use **Relationship Attributes**, an extra feature of **SQLModel** (and SQLAlchemy), to work with the data in the database in a much more familiar way, and closer to normal Python code. -!!! info - When I say "**relationship**" I mean the standard dictionary term, of data related to other data. +/// info - I'm not using the term "**relation**" that is the technical, academical, SQL term for a single table. +When I say "**relationship**" I mean the standard dictionary term, of data related to other data. + +I'm not using the term "**relation**" that is the technical, academical, SQL term for a single table. + +/// And using those **relationship attributes** is where a tool like **SQLModel** really shines. ✨ diff --git a/docs/tutorial/relationship-attributes/read-relationships.md b/docs/tutorial/relationship-attributes/read-relationships.md index 181b229589..a59ec8351e 100644 --- a/docs/tutorial/relationship-attributes/read-relationships.md +++ b/docs/tutorial/relationship-attributes/read-relationships.md @@ -6,26 +6,7 @@ Now that we know how to connect data using **relationship Attributes**, let's se First, add a function `select_heroes()` where we get a hero to start working with, and add that function to the `main()` function: -```Python hl_lines="3-7 14" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py[ln:96-100]!} - -# Previous code here omitted 👈 - -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py[ln:110-113]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py310.py ln[94:98,108:111] hl[94:98,111] *} ## Select the Related Team - Old Way @@ -33,75 +14,29 @@ Now that we have a hero, we can get the team this hero belongs to. With what we have learned **up to now**, we could use a `select()` statement, then execute it with `session.exec()`, and then get the `.first()` result, for example: -```Python hl_lines="9-12" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py[ln:96-105]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py310.py ln[94:103] hl[100:103] *} ## Get Relationship Team - New Way -But now that we have the **relationship attributes**, we can just access them, and **SQLModel** (actually SQLAlchemy) will go and fetch the correspoinding data from the database, and make it available in the attribute. ✨ +But now that we have the **relationship attributes**, we can just access them, and **SQLModel** (actually SQLAlchemy) will go and fetch the corresponding data from the database, and make it available in the attribute. ✨ So, the highlighted block above, has the same results as the block below: -```Python hl_lines="11" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py[ln:96-100]!} - - # Code from the previous example omitted 👈 - -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py[ln:107]!} +{* ./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py310.py ln[94:98,105] hl[105] *} -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py!} -``` +/// tip -
+The automatic data fetching will work as long as the starting object (in this case the `Hero`) is associated with an **open** session. -!!! tip - The automatic data fetching will work as long as the starting object (in this case the `Hero`) is associated with an **open** session. +For example, here, **inside** a `with` block with a `Session` object. - For example, here, **inside** a `with` block with a `Session` object. +/// ## Get a List of Relationship Objects And the same way, when we are working on the **many** side of the **one-to-many** relationship, we can get a list of of the related objects just by accessing the relationship attribute: -```Python hl_lines="9" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002.py[ln:96-102]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002.py!} -``` - -
+{* ./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py310.py ln[94:100] hl[100] *} That would print a list with all the heroes in the Preventers team: @@ -111,8 +46,8 @@ That would print a list with all the heroes in the Preventers team: $ python app.py // Automatically fetch the heroes -INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age, hero.team_id AS hero_team_id -FROM hero +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age, hero.team_id AS hero_team_id +FROM hero WHERE ? = hero.team_id INFO Engine [cached since 0.8774s ago] (2,) diff --git a/docs/tutorial/relationship-attributes/remove-relationships.md b/docs/tutorial/relationship-attributes/remove-relationships.md index ae4c6cfea2..ff408e20b9 100644 --- a/docs/tutorial/relationship-attributes/remove-relationships.md +++ b/docs/tutorial/relationship-attributes/remove-relationships.md @@ -8,41 +8,11 @@ And then for some reason needs to leave the **Preventers** for some years. 😭 We can remove the relationship by setting it to `None`, the same as with the `team_id`, it also works with the new relationship attribute `.team`: -```Python hl_lines="9" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002.py[ln:105-116]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002.py!} -``` - -
+{* ./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py310.py ln[103:114] hl[109] *} And of course, we should remember to add this `update_heroes()` function to `main()` so that it runs when we call this program from the command line: -```Python hl_lines="7" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002.py[ln:119-123]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002.py!} -``` - -
+{* ./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py310.py ln[117:121] hl[121] *} ## Recap diff --git a/docs/tutorial/relationship-attributes/type-annotation-strings.md b/docs/tutorial/relationship-attributes/type-annotation-strings.md index da77dad6bb..74b97f08d1 100644 --- a/docs/tutorial/relationship-attributes/type-annotation-strings.md +++ b/docs/tutorial/relationship-attributes/type-annotation-strings.md @@ -1,23 +1,10 @@ -## About the String in `List["Hero"]` +## About the String in `list["Hero"]` -In the first Relationship attribute, we declare it with `List["Hero"]`, putting the `Hero` in quotes instead of just normally there: +In the first Relationship attribute, we declare it with `list["Hero"]`, putting the `Hero` in quotes instead of just normally there: -```Python hl_lines="11" -{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py[ln:1-21]!} +{* ./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py310.py ln[1:19] hl[9] *} -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py!} -``` - -
- -What's that about? Can't we just write it normally as `List[Hero]`? +What's that about? Can't we just write it normally as `list[Hero]`? By that point, in that line in the code, the Python interpreter **doesn't know of any class `Hero`**, and if we put it just there, it would try to find it unsuccessfully, and then fail. 😭 @@ -29,5 +16,8 @@ And of course, **SQLModel** can also understand it in the string correctly. ✨ That is actually part of Python, it's the current official solution to handle it. -!!! info - There's a lot of work going on in Python itself to make that simpler and more intuitive, and find ways to make it possible to not wrap the class in a string. +/// info + +There's a lot of work going on in Python itself to make that simpler and more intuitive, and find ways to make it possible to not wrap the class in a string. + +/// diff --git a/docs/tutorial/select.md b/docs/tutorial/select.md index b5a092224f..3e20372463 100644 --- a/docs/tutorial/select.md +++ b/docs/tutorial/select.md @@ -23,14 +23,7 @@ Things are getting more exciting! Let's now see how to read data from the databa Let's continue from the last code we used to create some data. -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/insert/tutorial002.py!} -``` - -
+{* ./docs_src/tutorial/insert/tutorial002_py310.py ln[0] *} We are creating a **SQLModel** `Hero` class model and creating some records. @@ -79,16 +72,19 @@ You can try that out in **DB Browser for SQLite**: -!!! warning - Here we are getting all the rows. +/// warning + +Here we are getting all the rows. + +If you have thousands of rows, that could be expensive to compute for the database. - If you have thousands of rows, that could be expensive to compute for the database. +You would normally want to filter the rows to receive only the ones you want. But we'll learn about that later in the next chapter. - You would normally want to filter the rows to receive only the ones you want. But we'll learn about that later in the next chapter. +/// ### A SQL Shortcut -If we want to get all the columns like in this case above, in SQL there's a shortcut, instead of specifying each of the column names wew could write a `*`: +If we want to get all the columns like in this case above, in SQL there's a shortcut, instead of specifying each of the column names we could write a `*`: ```SQL SELECT * @@ -164,22 +160,7 @@ The first step is to create a **Session**, the same way we did when creating the We will start with that in a new function `select_heroes()`: -```Python hl_lines="3-4" -# Code above omitted 👆 - -{!./docs_src/tutorial/select/tutorial001.py[ln:36-37]!} - -# More code here later 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/select/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/select/tutorial001_py310.py ln[34:35] hl[34:35] *} ## Create a `select` Statement @@ -187,41 +168,11 @@ Next, pretty much the same way we wrote a SQL `SELECT` statement above, now we'l First we have to import `select` from `sqlmodel` at the top of the file: -```Python hl_lines="3" -{!./docs_src/tutorial/select/tutorial001.py[ln:1-3]!} - -# More code below ommitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/select/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/select/tutorial001_py310.py ln[1] hl[1] *} And then we will use it to create a `SELECT` statement in Python code: -```Python hl_lines="9" -{!./docs_src/tutorial/select/tutorial001.py[ln:1-3]!} - -# More code here omitted 👈 - -{!./docs_src/tutorial/select/tutorial001.py[ln:36-38]!} - -# More code here later 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/select/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/select/tutorial001_py310.py ln[1,34:36] hl[36] *} It's a very simple line of code that conveys a lot of information: @@ -240,31 +191,19 @@ We pass the class model `Hero` to the `select()` function. And that tells it tha And notice that in the `select()` function we don't explicitly specify the `FROM` part. It is already obvious to **SQLModel** (actually to SQLAlchemy) that we want to select `FROM` the table `hero`, because that's the one associated with the `Hero` class model. -!!! tip - The value of the `statement` returned by `select()` is a special object that allows us to do other things. - - I'll tell you about that in the next chapters. - -## Execute the Statement - -Now that we have the `select` statement, we can execute it with the **session**: +/// tip -```Python hl_lines="6" -# Code above omitted 👆 +The value of the `statement` returned by `select()` is a special object that allows us to do other things. -{!./docs_src/tutorial/select/tutorial001.py[ln:36-39]!} +I'll tell you about that in the next chapters. -# More code here later 👇 -``` +/// -
-👀 Full file preview +## Execute the Statement -```Python -{!./docs_src/tutorial/select/tutorial001.py!} -``` +Now that we have the `select` statement, we can execute it with the **session**: -
+{* ./docs_src/tutorial/select/tutorial001_py310.py ln[34:37] hl[37] *} This will tell the **session** to go ahead and use the **engine** to execute that `SELECT` statement in the database and bring the results back. @@ -274,7 +213,7 @@ This `session.exec(statement)` will generate this output: ``` INFO Engine BEGIN (implicit) -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age FROM hero INFO Engine [no key 0.00032s] () ``` @@ -302,22 +241,7 @@ The `results` object is an -👀 Full file preview - -```Python -{!./docs_src/tutorial/select/tutorial001.py!} -``` - - +{* ./docs_src/tutorial/select/tutorial001_py310.py ln[34:39] hl[38:39] *} This will print the output: @@ -331,37 +255,39 @@ id=3 name='Rusty-Man' age=48 secret_name='Tommy Sharp' Now include a call to `select_heroes()` in the `main()` function so that it is executed when we run the program from the command line: -```Python hl_lines="14" -# Code above omitted 👆 +{* ./docs_src/tutorial/select/tutorial001_py310.py ln[34:45] hl[45] *} -{!./docs_src/tutorial/select/tutorial001.py[ln:36-47]!} +## Review The Code -# More code here later 👇 -``` +Great, you're now being able to read the data from the database! 🎉 -
-👀 Full file preview +Let's review the code up to this point: -```Python -{!./docs_src/tutorial/select/tutorial001.py!} -``` +//// tab | Python 3.10+ -
+```{ .python .annotate } +{!./docs_src/tutorial/select/tutorial002_py310.py!} +``` -## Review The Code +{!./docs_src/tutorial/select/annotations/en/tutorial002.md!} -Great, you're now being able to read the data from the database! 🎉 +//// -Let's review the code up to this point: +//// tab | Python 3.9+ ```{ .python .annotate } -{!./docs_src/tutorial/select/tutorial002.py!} +{!./docs_src/tutorial/select/tutorial002_py39.py!} ``` {!./docs_src/tutorial/select/annotations/en/tutorial002.md!} -!!! tip - Check out the number bubbles to see what is done by each line of code. +//// + +/// tip + +Check out the number bubbles to see what is done by each line of code. + +/// Here it starts to become more evident why we should have a single **engine** for the whole application, but different **sessions** for each group of operations. @@ -373,10 +299,13 @@ And the second section reading data from the database could be in another functi So, both sections could be in **different places** and would need their own sessions. -!!! info - To be fair, in this example all that code could actually share the same **session**, there's actually no need to have two here. +/// info + +To be fair, in this example all that code could actually share the same **session**, there's actually no need to have two here. - But it allows me to show you how they could be separated and to reinforce the idea that you should have **one engine** per application, and **multiple sessions**, one per each group of operations. +But it allows me to show you how they could be separated and to reinforce the idea that you should have **one engine** per application, and **multiple sessions**, one per each group of operations. + +/// ## Get a List of `Hero` Objects @@ -386,22 +315,7 @@ But for different reasons you might want to have the full **list of `Hero`** obj The special `results` object also has a method `results.all()` that returns a list with all the objects: -```Python hl_lines="7" -# Code above omitted 👆 - -{!./docs_src/tutorial/select/tutorial003.py[ln:36-41]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/select/tutorial003.py!} -``` - -
+{* ./docs_src/tutorial/select/tutorial003_py310.py ln[34:39] hl[38] *} With this now we have all the heroes in a list in the `heroes` variable. @@ -415,8 +329,11 @@ After printing it, we would see something like: ] ``` -!!! info - It would actually look more compact, I'm formatting it a bit for you to see that it is actually a list with all the data. +/// info + +It would actually look more compact, I'm formatting it a bit for you to see that it is actually a list with all the data. + +/// ## Compact Version @@ -424,22 +341,7 @@ I have been creating several variables to be able to explain to you what each th But knowing what is each object and what it is all doing, we can simplify it a bit and put it in a more compact form: -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/select/tutorial004.py[ln:36-39]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/select/tutorial004.py!} -``` - -
+{* ./docs_src/tutorial/select/tutorial004_py310.py ln[34:37] hl[36] *} Here we are putting it all on a single line, you will probably put the select statements in a single line like this more often. @@ -455,14 +357,17 @@ In this chapter we are touching some of them. ### SQLModel's `select` -When importing from `sqlmodel` the `select()` function, you are using **SQLModel**'s version of `select`. +When importing from `sqlmodel` the `select()` function, you are using **SQLModel**'s version of `select`. -SQLAchemy also has it's own `select`, and SQLModel's `select` uses SQLAlchemy's `select` internally. +SQLAchemy also has its own `select`, and SQLModel's `select` uses SQLAlchemy's `select` internally. But SQLModel's version does a lot of **tricks** with type annotations to make sure you get the best **editor support** possible, no matter if you use **VS Code**, **PyCharm**, or something else. ✨ -!!! info - There was a lot of work and research, with different versions of the internal code, to improve this as much as possible. 🤓 +/// info + +There was a lot of work and research, with different versions of the internal code, to improve this as much as possible. 🤓 + +/// ### SQLModel's `session.exec` @@ -472,7 +377,7 @@ SQLAlchemy's own `Session` has a method `session.execute()`. It doesn't have a ` If you see SQLAlchemy tutorials, they will always use `session.execute()`. -**SQLModel**'s own `Session` inherits directly from SQLAlchemy's `Session`, and adds this additonal method `session.exec()`. Underneath, it uses the same `session.execute()`. +**SQLModel**'s own `Session` inherits directly from SQLAlchemy's `Session`, and adds this additional method `session.exec()`. Underneath, it uses the same `session.execute()`. But `session.exec()` does several **tricks** combined with the tricks in `session()` to give you the **best editor support**, with **autocompletion** and **inline errors** everywhere, even after getting data from a select. ✨ @@ -492,10 +397,13 @@ On top of that, **SQLModel**'s `session.exec()` also does some tricks to reduce But SQLModel's `Session` still has access to `session.execute()` too. -!!! tip - Your editor will give you autocompletion for both `session.exec()` and `session.execute()`. +/// tip + +Your editor will give you autocompletion for both `session.exec()` and `session.execute()`. + +📢 Remember to **always use `session.exec()`** to get the best editor support and developer experience. - 📢 Remember to **always use `session.exec()`** to get the best editor support and developer experience. +/// ### Caveats of **SQLModel** Flavor diff --git a/docs/tutorial/update.md b/docs/tutorial/update.md index b3099f5a16..7c7cb5c45d 100644 --- a/docs/tutorial/update.md +++ b/docs/tutorial/update.md @@ -6,14 +6,7 @@ Now let's see how to update data using **SQLModel**. As before, we'll continue from where we left off with the previous code. -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/indexes/tutorial002.py!} -``` - -
+{* ./docs_src/tutorial/indexes/tutorial002_py310.py ln[0] *} Remember to remove the `database.db` file before running the examples to get the same results. @@ -41,12 +34,15 @@ And the second part, with the `WHERE`, defines to which rows it should apply tha In this case, as we only have one hero with the name `"Spider-Boy"`, it will only apply the update in that row. -!!! info - Notice that in the `UPDATE` the single equals sign (`=`) means **assignment**, setting a column to some value. +/// info - And in the `WHERE` the same single equals sign (`=`) is used for **comparison** between two values, to find rows that match. +Notice that in the `UPDATE` the single equals sign (`=`) means **assignment**, setting a column to some value. - This is in contrast to Python and most programming languages, where a single equals sign (`=`) is used for assignment, and two equal signs (`==`) are used for comparisons. +And in the `WHERE` the same single equals sign (`=`) is used for **comparison** between two values, to find rows that match. + +This is in contrast to Python and most programming languages, where a single equals sign (`=`) is used for assignment, and two equal signs (`==`) are used for comparisons. + +/// You can try that in **DB Browser for SQLite**: @@ -69,16 +65,19 @@ After that update, the data in the table will look like this, with the new age f -!!! tip - It will probably be more common to find the row to update by `id`, for example: +/// tip - ```SQL - UPDATE hero - SET age=16 - WHERE id = 2 - ``` +It will probably be more common to find the row to update by `id`, for example: - But in the example above I used `name` to make it more intuitive. +```SQL +UPDATE hero +SET age=16 +WHERE id = 2 +``` + +But in the example above I used `name` to make it more intuitive. + +/// Now let's do the same update in code, with **SQLModel**. @@ -88,39 +87,11 @@ To get the same results, delete the `database.db` file before running the exampl We'll start by selecting the hero `"Spider-Boy"`, this is the one we will update: -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/update/tutorial001.py[ln:44-49]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/update/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/update/tutorial001_py310.py ln[42:47] hl[44] *} Let's not forget to add that `update_heroes()` function to the `main()` function so that we call it when executing the program from the command line: -```Python hl_lines="6" -# Code above omitted 👆 - -{!./docs_src/tutorial/update/tutorial001.py[ln:58-65]!} -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/update/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/update/tutorial001_py310.py ln[56:63] hl[59] *} Up to that point, running that in the command line will output: @@ -132,8 +103,8 @@ $ python app.py // Some boilerplate and previous output omitted 😉 // The SELECT with WHERE -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.name = ? INFO Engine [no key 0.00017s] ('Spider-Boy',) @@ -143,8 +114,11 @@ Hero: name='Spider-Boy' secret_name='Pedro Parqueador' age=None id=2
-!!! tip - Notice that by this point, the hero still doesn't have an age. +/// tip + +Notice that by this point, the hero still doesn't have an age. + +/// ## Set a Field Value @@ -152,22 +126,7 @@ Now that you have a `hero` object, you can simply set the value of the field (th In this case, we will set the `age` to `16`: -```Python hl_lines="10" -# Code above omitted 👆 - -{!./docs_src/tutorial/update/tutorial001.py[ln:44-51]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/update/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/update/tutorial001_py310.py ln[42:49] hl[49] *} ## Add the Hero to the Session @@ -175,22 +134,7 @@ Now that the hero object in memory has a change, in this case a new value for th This is the same we did when creating new hero instances: -```Python hl_lines="11" -# Code above omitted 👆 - -{!./docs_src/tutorial/update/tutorial001.py[ln:44-52]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/update/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/update/tutorial001_py310.py ln[42:50] hl[50] *} ## Commit the Session @@ -198,22 +142,7 @@ To save the current changes in the session, **commit** it. This will save the updated hero in the database: -```Python hl_lines="12" -# Code above omitted 👆 - -{!./docs_src/tutorial/update/tutorial001.py[ln:44-53]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/update/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/update/tutorial001_py310.py ln[42:51] hl[51] *} It will also save anything else that was added to the session. @@ -246,22 +175,7 @@ The data in the object would be automatically refreshed if we accessed an attrib But in this example we are not accessing any attribute, we will only print the object. And we also want to be explicit, so we will `.refresh()` the object directly: -```Python hl_lines="13" -# Code above omitted 👆 - -{!./docs_src/tutorial/update/tutorial001.py[ln:44-54]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/update/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/update/tutorial001_py310.py ln[42:52] hl[52] *} This refresh will trigger the same SQL query that would be automatically triggered by accessing an attribute. So it will generate this output: @@ -275,8 +189,8 @@ $ python app.py // Previous output omitted 🙈 // The SQL to SELECT the fresh hero data -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.id = ? INFO Engine [generated in 0.00018s] (2,) ``` @@ -287,22 +201,7 @@ INFO Engine [generated in 0.00018s] (2,) Now we can just print the hero: -```Python hl_lines="14" -# Code above omitted 👆 - -{!./docs_src/tutorial/update/tutorial001.py[ln:44-55]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/update/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/update/tutorial001_py310.py ln[42:53] hl[53] *} Because we refreshed it right after updating it, it has fresh data, including the new `age` we just updated. @@ -327,14 +226,31 @@ Updated hero: name='Spider-Boy' secret_name='Pedro Parqueador' age=16 id=2 Now let's review all that code: +//// tab | Python 3.10+ + +```{ .python .annotate hl_lines="42-53" } +{!./docs_src/tutorial/update/tutorial002_py310.py!} +``` + +{!./docs_src/tutorial/update/annotations/en/tutorial002.md!} + +//// + +//// tab | Python 3.9+ + ```{ .python .annotate hl_lines="44-55" } -{!./docs_src/tutorial/update/tutorial002.py!} +{!./docs_src/tutorial/update/tutorial002_py39.py!} ``` {!./docs_src/tutorial/update/annotations/en/tutorial002.md!} -!!! tip - Check out the number bubbles to see what is done by each line of code. +//// + +/// tip + +Check out the number bubbles to see what is done by each line of code. + +/// ## Multiple Updates @@ -342,27 +258,59 @@ The update process with **SQLModel** is more or less the same as with creating n This also means that you can update several fields (attributes, columns) at once, and you can also update several objects (heroes) at once: +//// tab | Python 3.10+ + +```{ .python .annotate hl_lines="15-17 19-21 23" } +# Code above omitted 👆 + +{!./docs_src/tutorial/update/tutorial004_py310.py[ln:42-68]!} + +# Code below omitted 👇 +``` + +{!./docs_src/tutorial/update/annotations/en/tutorial004.md!} + +//// + +//// tab | Python 3.9+ + ```{ .python .annotate hl_lines="15-17 19-21 23" } # Code above omitted 👆 -{!./docs_src/tutorial/update/tutorial004.py[ln:44-70]!} +{!./docs_src/tutorial/update/tutorial004_py39.py[ln:44-70]!} # Code below omitted 👇 ``` {!./docs_src/tutorial/update/annotations/en/tutorial004.md!} -
-👀 Full file preview +//// + +/// details | 👀 Full file preview + +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/update/tutorial004_py310.py!} +``` + +//// + +//// tab | Python 3.9+ ```Python -{!./docs_src/tutorial/update/tutorial004.py!} +{!./docs_src/tutorial/update/tutorial004_py39.py!} ``` -
+//// + +/// + +/// tip + +Review what each line does by clicking each number bubble in the code. 👆 -!!! tip - Review what each line does by clicking each number bubble in the code. 👆 +/// ## Recap diff --git a/docs/tutorial/where.md b/docs/tutorial/where.md index 45e909cc75..b6d08e72fa 100644 --- a/docs/tutorial/where.md +++ b/docs/tutorial/where.md @@ -31,14 +31,7 @@ We'll continue with the same examples we have been using in the previous chapter And now we will update `select_heroes()` to filter the data. -
-👀 Full file preview - -```Python hl_lines="36-41" -{!./docs_src/tutorial/select/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/select/tutorial001_py310.py ln[0] *} If you already executed the previous examples and have a database with data, **remove the database file** before running each example, that way you won't have duplicate data and you will be able to get the same results. @@ -81,10 +74,13 @@ Then the database will bring a table like this: -!!! tip - Even if the result is only one row, the database always returns a **table**. +/// tip + +Even if the result is only one row, the database always returns a **table**. + +In this case, a table with only one row. - In this case, a table with only one row. +/// You can try that out in **DB Browser for SQLite**: @@ -187,43 +183,13 @@ Let's review some of the code we used to read data with **SQLModel**. We care specially about the **select** statement: -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/select/tutorial001.py[ln:36-41]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/select/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/select/tutorial001_py310.py ln[34:39] hl[36] *} ## Filter Rows Using `WHERE` with **SQLModel** -Now, the same way that we add `WHERE` to a SQL statement to filter rows, we can add a `.where()` to a **SQLModel** `select()` statment to filter rows, which will filter the objects returned: +Now, the same way that we add `WHERE` to a SQL statement to filter rows, we can add a `.where()` to a **SQLModel** `select()` statement to filter rows, which will filter the objects returned: -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/where/tutorial001.py[ln:36-41]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/where/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/where/tutorial001_py310.py ln[34:39] hl[36] *} It's a very small change, but it's packed of details. Let's explore them. @@ -268,10 +234,13 @@ So, what's happening there? In the example above we are using two equal signs (`==`). That's called the "**equality operator**". -!!! tip - An **operator** is just a symbol that is put beside one value or in the middle of two values to do something with them. +/// tip + +An **operator** is just a symbol that is put beside one value or in the middle of two values to do something with them. - `==` is called the **equality** operator because it checks if two things are **equal**. +`==` is called the **equality** operator because it checks if two things are **equal**. + +/// When writing Python, if you write something using this equality operator (`==`) like: @@ -291,8 +260,11 @@ True False ``` -!!! tip - `<`, `>`, `==`, `>=`, `<=`, and `!=` are all **operators** used for **comparisons**. +/// tip + +`<`, `>`, `==`, `>=`, `<=`, and `!=` are all **operators** used for **comparisons**. + +/// But SQLAlchemy adds some magic to the columns/fields in a **model class** to make those Python comparisons have super powers. @@ -311,7 +283,7 @@ Instead, it results in a special type of object. If you tried that in an interac ``` -So, that result value is an **expession** object. 💡 +So, that result value is an **expression** object. 💡 And `.where()` takes one (or more) of these **expression** objects to update the SQL statement. @@ -421,7 +393,7 @@ Of course, the keyword arguments would have been a bit shorter. But with the **expressions** your editor can help you a lot with autocompletion and inline error checks. ✨ -Let me give you an example. Let's imagine that keword arguments were supported in SQLModel and you wanted to filter using the secret identity of Spider-Boy. +Let me give you an example. Let's imagine that keyword arguments were supported in SQLModel and you wanted to filter using the secret identity of Spider-Boy. You could write: @@ -436,7 +408,7 @@ Maybe your code could even run and seem like it's all fine, and then some months And maybe finally you would realize that we wrote the code using `secret_identity` which is not a column in the table. We should have written `secret_name` instead. -Now, with the the expressions, your editor would show you an error right away if you tried this: +Now, with the expressions, your editor would show you an error right away if you tried this: ```Python # Expression ✨ @@ -451,8 +423,11 @@ select(Hero).where(Hero.secret_name == "Pedro Parqueador") I think that alone, having better editor support, autocompletion, and inline errors, is enough to make it worth having expressions instead of keyword arguments. ✨ -!!! tip - **Expressions** also provide more features for other types of comparisons, shown down below. 👇 +/// tip + +**Expressions** also provide more features for other types of comparisons, shown down below. 👇 + +/// ## Exec the Statement @@ -460,22 +435,7 @@ Now that we know how `.where()` works, let's finish the code. It's actually the same as in previous chapters for selecting data: -```Python hl_lines="6-8" -# Code above omitted 👆 - -{!./docs_src/tutorial/where/tutorial001.py[ln:36-41]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/where/tutorial001.py!} -``` - -
+{* ./docs_src/tutorial/where/tutorial001_py310.py ln[34:39] hl[37:39] *} We take that statement, that now includes a `WHERE`, and we `exec()` it to get the results. @@ -490,8 +450,8 @@ $ python app.py // Now the important part, the SELECT with WHERE 💡 -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.name = ? INFO Engine [no key 0.00014s] ('Deadpond',) @@ -502,12 +462,15 @@ secret_name='Dive Wilson' age=None id=1 name='Deadpond' -!!! tip - The `results` object is an iterable to be used in a `for` loop. +/// tip + +The `results` object is an iterable to be used in a `for` loop. - Even if we got only one row, we iterate over that `results` object. Just as if it was a list of one element. +Even if we got only one row, we iterate over that `results` object. Just as if it was a list of one element. - We'll see other ways to get the data later. +We'll see other ways to get the data later. + +/// ## Other Comparisons @@ -521,22 +484,7 @@ But we can use other standard Python comparisons. ✨ We could get the rows where a column is **not** equal to a value using `!=`: -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/where/tutorial002.py[ln:36-41]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/where/tutorial002.py!} -``` - -
+{* ./docs_src/tutorial/where/tutorial002_py310.py ln[34:39] hl[36] *} That would output: @@ -549,22 +497,7 @@ secret_name='Tommy Sharp' age=48 id=3 name='Rusty-Man' Let's update the function `create_heroes()` and add some more rows to make the next comparison examples clearer: -```Python hl_lines="4-10 13-19" -# Code above omitted 👆 - -{!./docs_src/tutorial/where/tutorial003.py[ln:23-41]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/where/tutorial003.py!} -``` - -
+{* ./docs_src/tutorial/where/tutorial003_py310.py ln[21:39] hl[22:28,31:37] *} Now that we have several heroes with different ages, it's gonna be more obvious what the next comparisons do. @@ -572,22 +505,7 @@ Now that we have several heroes with different ages, it's gonna be more obvious Now let's use `>` to get the rows where a column is **more than** a value: -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/where/tutorial003.py[ln:44-49]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/where/tutorial003.py!} -``` - -
+{* ./docs_src/tutorial/where/tutorial003_py310.py ln[42:47] hl[44] *} That would output: @@ -597,29 +515,17 @@ age=36 id=6 name='Dr. Weird' secret_name='Steve Weird' age=93 id=7 name='Captain North America' secret_name='Esteban Rogelios' ``` -!!! tip - Notice that it didn't select `Black Lion`, because the age is not *strictly* greater than `35`. - -### More Than or Equal - -Let's do that again, but with `>=` to get the rows where a column is **more than or equal** to a value: +/// tip -```Python hl_lines="5" -# Code above omitted 👆 +Notice that it didn't select `Black Lion`, because the age is not *strictly* greater than `35`. -{!./docs_src/tutorial/where/tutorial004.py[ln:44-49]!} +/// -# Code below omitted 👇 -``` - -
-👀 Full file preview +### More Than or Equal -```Python -{!./docs_src/tutorial/where/tutorial004.py!} -``` +Let's do that again, but with `>=` to get the rows where a column is **more than or equal** to a value: -
+{* ./docs_src/tutorial/where/tutorial004_py310.py ln[42:47] hl[44] *} Because we are using `>=`, the age `35` will be included in the output: @@ -630,29 +536,17 @@ age=36 id=6 name='Dr. Weird' secret_name='Steve Weird' age=93 id=7 name='Captain North America' secret_name='Esteban Rogelios' ``` -!!! tip - This time we got `Black Lion` too because although the age is not *strictly* greater than `35`it is *equal* to `35`. - -### Less Than - -Similarly, we can use `<` to get the rows where a column is **less than** a value: +/// tip -```Python hl_lines="5" -# Code above omitted 👆 +This time we got `Black Lion` too because although the age is not *strictly* greater than `35`it is *equal* to `35`. -{!./docs_src/tutorial/where/tutorial005.py[ln:44-49]!} - -# Code below omitted 👇 -``` +/// -
-👀 Full file preview +### Less Than -```Python -{!./docs_src/tutorial/where/tutorial005.py!} -``` +Similarly, we can use `<` to get the rows where a column is **less than** a value: -
+{* ./docs_src/tutorial/where/tutorial005_py310.py ln[42:47] hl[44] *} And we get the younger one with an age in the database: @@ -660,29 +554,17 @@ And we get the younger one with an age in the database: age=32 id=4 name='Tarantula' secret_name='Natalia Roman-on' ``` -!!! tip - We could imagine that **Spider-Boy** is even **younger**. But because we don't know the age, it is `NULL` in the database (`None` in Python), it doesn't match any of these age comparisons with numbers. +/// tip -### Less Than or Equal +We could imagine that **Spider-Boy** is even **younger**. But because we don't know the age, it is `NULL` in the database (`None` in Python), it doesn't match any of these age comparisons with numbers. -Finally, we can use `<=` to get the rows where a column is **less than or equal** to a value: - -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/where/tutorial006.py[ln:44-49]!} - -# Code below omitted 👇 -``` +/// -
-👀 Full file preview +### Less Than or Equal -```Python -{!./docs_src/tutorial/where/tutorial006.py!} -``` +Finally, we can use `<=` to get the rows where a column is **less than or equal** to a value: -
+{* ./docs_src/tutorial/where/tutorial006_py310.py ln[42:47] hl[44] *} And we get the younger ones, `35` and below: @@ -691,10 +573,13 @@ age=32 id=4 name='Tarantula' secret_name='Natalia Roman-on' age=35 id=5 name='Black Lion' secret_name='Trevor Challa' ``` -!!! tip - We get `Black Lion` here too because although the age is not *strictly* less than `35` it is *equal* to `35`. +/// tip -### Benefits of Expresions +We get `Black Lion` here too because although the age is not *strictly* less than `35` it is *equal* to `35`. + +/// + +### Benefits of Expressions Here's a good moment to see that being able to use these pure Python expressions instead of keyword arguments can help a lot. ✨ @@ -704,30 +589,15 @@ We can use the same standard Python comparison operators like `<`, `<=`, `>`, `> Because `.where()` returns the same special select object back, we can add more `.where()` calls to it: -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/where/tutorial007.py[ln:44-49]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/where/tutorial007.py!} -``` - -
+{* ./docs_src/tutorial/where/tutorial007_py310.py ln[42:47] hl[44] *} This will select the rows `WHERE` the `age` is **greater than or equal** to `35`, `AND` also the `age` is **less than** `40`. The equivalent SQL would be: ```SQL hl_lines="3" -SELECT id, name, secret_name, age -FROM hero +SELECT id, name, secret_name, age +FROM hero WHERE age >= 35 AND age < 40 ``` @@ -743,12 +613,12 @@ $ python app.py // Some boilerplate output omitted 😉 // The SELECT statement with WHERE, also using AND -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.age >= ? AND hero.age < ? INFO Engine [no key 0.00014s] (35, 40) -// The two heros printed +// The two heroes printed age=35 id=5 name='Black Lion' secret_name='Trevor Challa' age=36 id=6 name='Dr. Weird' secret_name='Steve Weird' @@ -760,22 +630,7 @@ age=36 id=6 name='Dr. Weird' secret_name='Steve Weird' As an alternative to using multiple `.where()` we can also pass several expressions to a single `.where()`: -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/where/tutorial008.py[ln:44-49]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/where/tutorial008.py!} -``` - -
+{* ./docs_src/tutorial/where/tutorial008_py310.py ln[42:47] hl[44] *} This is the same as the above, and will result in the same output with the two heroes: @@ -792,41 +647,13 @@ But we can also combine expressions using `OR`. Which means that **any** (but no To do it, you can import `or_`: -```Python hl_lines="3" -{!./docs_src/tutorial/where/tutorial009.py[ln:1-3]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/where/tutorial009.py!} -``` - -
+{* ./docs_src/tutorial/where/tutorial009_py310.py ln[1] hl[1] *} And then pass both expressions to `or_()` and put it inside `.where()`. For example, here we select the heroes that are the youngest OR the oldest: -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/where/tutorial009.py[ln:44-49]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/where/tutorial009.py!} -``` - -
+{* ./docs_src/tutorial/where/tutorial009_py310.py ln[42:47] hl[44] *} When we run it, this generates the output: @@ -838,8 +665,8 @@ $ python app.py // Some boilerplate output omitted 😉 // The SELECT statement with WHERE, also using OR 🔍 -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.age <= ? OR hero.age > ? INFO Engine [no key 0.00021s] (35, 90) @@ -863,9 +690,9 @@ It would be an error telling you that > `Hero.age` is potentially `None`, and you cannot compare `None` with `>` -This is because as we are using pure and plain Python annotations for the fields, `age` is indeed annotated as `Optional[int]`, which means `int` or `None`. +This is because as we are using pure and plain Python annotations for the fields, `age` is indeed annotated as `int | None`. -By using this simple and standard Python type annotations We get the benefit of the extra simplicity and the inline error checks when creating or using instances. ✨ +By using this simple and standard Python type annotations we get the benefit of the extra simplicity and the inline error checks when creating or using instances. ✨ And when we use these special **class attributes** in a `.where()`, during execution of the program, the special class attribute will know that the comparison only applies for the values that are not `NULL` in the database, and it will work correctly. @@ -877,39 +704,11 @@ We can tell the editor that this class attribute is actually a special **SQLMode To do that, we can import `col()` (as short for "column"): -```Python hl_lines="3" -{!./docs_src/tutorial/where/tutorial011.py[ln:1-3]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/where/tutorial011.py!} -``` - -
+{* ./docs_src/tutorial/where/tutorial011_py310.py ln[1] hl[1] *} And then put the **class attribute** inside `col()` when using it in a `.where()`: -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/where/tutorial011.py[ln:44-49]!} - -# Code below omitted 👇 -``` - -
-👀 Full file preview - -```Python -{!./docs_src/tutorial/where/tutorial011.py!} -``` - -
+{* ./docs_src/tutorial/where/tutorial011_py310.py ln[42:47] hl[44] *} So, now the comparison is not: @@ -925,10 +724,13 @@ col(Hero.age) > 35 And with that the editor knows this code is actually fine, because this is a special **SQLModel** column. -!!! tip - That `col()` will come handy later, giving autocompletion to several other things we can do with these special **class attributes** for columns. +/// tip + +That `col()` will come handy later, giving autocompletion to several other things we can do with these special **class attributes** for columns. + +But we'll get there later. - But we'll get there later. +/// ## Recap diff --git a/docs/virtual-environments.md b/docs/virtual-environments.md new file mode 100644 index 0000000000..131cd53d76 --- /dev/null +++ b/docs/virtual-environments.md @@ -0,0 +1,844 @@ +# Virtual Environments + +When you work in Python projects you probably should use a **virtual environment** (or a similar mechanism) to isolate the packages you install for each project. + +/// info + +If you already know about virtual environments, how to create them and use them, you might want to skip this section. 🤓 + +/// + +/// tip + +A **virtual environment** is different than an **environment variable**. + +An **environment variable** is a variable in the system that can be used by programs. + +A **virtual environment** is a directory with some files in it. + +/// + +/// info + +This page will teach you how to use **virtual environments** and how they work. + +If you are ready to adopt a **tool that manages everything** for you (including installing Python), try uv. + +/// + +## Create a Project + +First, create a directory for your project. + +What I normally do is that I create a directory named `code` inside my home/user directory. + +And inside of that I create one directory per project. + +
+ +```console +// Go to the home directory +$ cd +// Create a directory for all your code projects +$ mkdir code +// Enter into that code directory +$ cd code +// Create a directory for this project +$ mkdir awesome-project +// Enter into that project directory +$ cd awesome-project +``` + +
+ +## Create a Virtual Environment + +When you start working on a Python project **for the first time**, create a virtual environment **inside your project**. + +/// tip + +You only need to do this **once per project**, not every time you work. + +/// + +//// tab | `venv` + +To create a virtual environment, you can use the `venv` module that comes with Python. + +
+ +```console +$ python -m venv .venv +``` + +
+ +/// details | What that command means + +* `python`: use the program called `python` +* `-m`: call a module as a script, we'll tell it which module next +* `venv`: use the module called `venv` that normally comes installed with Python +* `.venv`: create the virtual environment in the new directory `.venv` + +/// + +//// + +//// tab | `uv` + +If you have `uv` installed, you can use it to create a virtual environment. + +
+ +```console +$ uv venv +``` + +
+ +/// tip + +By default, `uv` will create a virtual environment in a directory called `.venv`. + +But you could customize it passing an additional argument with the directory name. + +/// + +//// + +That command creates a new virtual environment in a directory called `.venv`. + +/// details | `.venv` or other name + +You could create the virtual environment in a different directory, but there's a convention of calling it `.venv`. + +/// + +## Activate the Virtual Environment + +Activate the new virtual environment so that any Python command you run or package you install uses it. + +/// tip + +Do this **every time** you start a **new terminal session** to work on the project. + +/// + +//// tab | Linux, macOS + +
+ +```console +$ source .venv/bin/activate +``` + +
+ +//// + +//// tab | Windows PowerShell + +
+ +```console +$ .venv\Scripts\Activate.ps1 +``` + +
+ +//// + +//// tab | Windows Bash + +Or if you use Bash for Windows (e.g. Git Bash): + +
+ +```console +$ source .venv/Scripts/activate +``` + +
+ +//// + +/// tip + +Every time you install a **new package** in that environment, **activate** the environment again. + +This makes sure that if you use a **terminal (CLI) program** installed by that package, you use the one from your virtual environment and not any other that could be installed globally, probably with a different version than what you need. + +/// + +## Check the Virtual Environment is Active + +Check that the virtual environment is active (the previous command worked). + +/// tip + +This is **optional**, but it's a good way to **check** that everything is working as expected and you are using the virtual environment you intended. + +/// + +//// tab | Linux, macOS, Windows Bash + +
+ +```console +$ which python + +/home/user/code/awesome-project/.venv/bin/python +``` + +
+ +If it shows the `python` binary at `.venv/bin/python`, inside of your project (in this case `awesome-project`), then it worked. 🎉 + +//// + +//// tab | Windows PowerShell + +
+ +```console +$ Get-Command python + +C:\Users\user\code\awesome-project\.venv\Scripts\python +``` + +
+ +If it shows the `python` binary at `.venv\Scripts\python`, inside of your project (in this case `awesome-project`), then it worked. 🎉 + +//// + +## Upgrade `pip` + +/// tip + +If you use `uv` you would use it to install things instead of `pip`, so you don't need to upgrade `pip`. 😎 + +/// + +If you are using `pip` to install packages (it comes by default with Python), you should **upgrade** it to the latest version. + +Many exotic errors while installing a package are solved by just upgrading `pip` first. + +/// tip + +You would normally do this **once**, right after you create the virtual environment. + +/// + +Make sure the virtual environment is active (with the command above) and then run: + +
+ +```console +$ python -m pip install --upgrade pip + +---> 100% +``` + +
+ +## Add `.gitignore` + +If you are using **Git** (you should), add a `.gitignore` file to exclude everything in your `.venv` from Git. + +/// tip + +If you used `uv` to create the virtual environment, it already did this for you, you can skip this step. 😎 + +/// + +/// tip + +Do this **once**, right after you create the virtual environment. + +/// + +
+ +```console +$ echo "*" > .venv/.gitignore +``` + +
+ +/// details | What that command means + +* `echo "*"`: will "print" the text `*` in the terminal (the next part changes that a bit) +* `>`: anything printed to the terminal by the command to the left of `>` should not be printed but instead written to the file that goes to the right of `>` +* `.gitignore`: the name of the file where the text should be written + +And `*` for Git means "everything". So, it will ignore everything in the `.venv` directory. + +That command will create a file `.gitignore` with the content: + +```gitignore +* +``` + +/// + +## Install Packages + +After activating the environment, you can install packages in it. + +/// tip + +Do this **once** when installing or upgrading the packages your project needs. + +If you need to upgrade a version or add a new package you would **do this again**. + +/// + +### Install Packages Directly + +If you're in a hurry and don't want to use a file to declare your project's package requirements, you can install them directly. + +/// tip + +It's a (very) good idea to put the packages and versions your program needs in a file (for example `requirements.txt` or `pyproject.toml`). + +/// + +//// tab | `pip` + +
+ +```console +$ pip install sqlmodel + +---> 100% +``` + +
+ +//// + +//// tab | `uv` + +If you have `uv`: + +
+ +```console +$ uv pip install sqlmodel +---> 100% +``` + +
+ +//// + +### Install from `requirements.txt` + +If you have a `requirements.txt`, you can now use it to install its packages. + +//// tab | `pip` + +
+ +```console +$ pip install -r requirements.txt +---> 100% +``` + +
+ +//// + +//// tab | `uv` + +If you have `uv`: + +
+ +```console +$ uv pip install -r requirements.txt +---> 100% +``` + +
+ +//// + +/// details | `requirements.txt` + +A `requirements.txt` with some packages could look like: + +```requirements.txt +sqlmodel==0.13.0 +rich==13.7.1 +``` + +/// + +## Run Your Program + +After you activated the virtual environment, you can run your program, and it will use the Python inside of your virtual environment with the packages you installed there. + +
+ +```console +$ python main.py + +Hello World +``` + +
+ +## Configure Your Editor + +You would probably use an editor, make sure you configure it to use the same virtual environment you created (it will probably autodetect it) so that you can get autocompletion and inline errors. + +For example: + +* VS Code +* PyCharm + +/// tip + +You normally have to do this only **once**, when you create the virtual environment. + +/// + +## Deactivate the Virtual Environment + +Once you are done working on your project you can **deactivate** the virtual environment. + +
+ +```console +$ deactivate +``` + +
+ +This way, when you run `python` it won't try to run it from that virtual environment with the packages installed there. + +## Ready to Work + +Now you're ready to start working on your project. + + + +/// tip + +Do you want to understand what's all that above? + +Continue reading. 👇🤓 + +/// + +## Why Virtual Environments + +To work with SQLModel you need to install Python. + +After that, you would need to **install** SQLModel and any other **packages** you want to use. + +To install packages you would normally use the `pip` command that comes with Python (or similar alternatives). + +Nevertheless, if you just use `pip` directly, the packages would be installed in your **global Python environment** (the global installation of Python). + +### The Problem + +So, what's the problem with installing packages in the global Python environment? + +At some point, you will probably end up writing many different programs that depend on **different packages**. And some of these projects you work on will depend on **different versions** of the same package. 😱 + +For example, you could create a project called `philosophers-stone`, this program depends on another package called **`harry`, using the version `1`**. So, you need to install `harry`. + +```mermaid +flowchart LR + stone(philosophers-stone) -->|requires| harry-1[harry v1] +``` + +Then, at some point later, you create another project called `prisoner-of-azkaban`, and this project also depends on `harry`, but this project needs **`harry` version `3`**. + +```mermaid +flowchart LR + azkaban(prisoner-of-azkaban) --> |requires| harry-3[harry v3] +``` + +But now the problem is, if you install the packages globally (in the global environment) instead of in a local **virtual environment**, you will have to choose which version of `harry` to install. + +If you want to run `philosophers-stone` you will need to first install `harry` version `1`, for example with: + +
+ +```console +$ pip install "harry==1" +``` + +
+ +And then you would end up with `harry` version `1` installed in your global Python environment. + +```mermaid +flowchart LR + subgraph global[global env] + harry-1[harry v1] + end + subgraph stone-project[philosophers-stone project] + stone(philosophers-stone) -->|requires| harry-1 + end +``` + +But then if you want to run `prisoner-of-azkaban`, you will need to uninstall `harry` version `1` and install `harry` version `3` (or just installing version `3` would automatically uninstall version `1`). + +
+ +```console +$ pip install "harry==3" +``` + +
+ +And then you would end up with `harry` version `3` installed in your global Python environment. + +And if you try to run `philosophers-stone` again, there's a chance it would **not work** because it needs `harry` version `1`. + +```mermaid +flowchart LR + subgraph global[global env] + harry-1[harry v1] + style harry-1 fill:#ccc,stroke-dasharray: 5 5 + harry-3[harry v3] + end + subgraph stone-project[philosophers-stone project] + stone(philosophers-stone) -.-x|⛔️| harry-1 + end + subgraph azkaban-project[prisoner-of-azkaban project] + azkaban(prisoner-of-azkaban) --> |requires| harry-3 + end +``` + +/// tip + +It's very common in Python packages to try the best to **avoid breaking changes** in **new versions**, but it's better to be safe, and install newer versions intentionally and when you can run the tests to check everything is working correctly. + +/// + +Now, imagine that with **many** other **packages** that all your **projects depend on**. That's very difficult to manage. And you would probably end up running some projects with some **incompatible versions** of the packages, and not knowing why something isn't working. + +Also, depending on your operating system (e.g. Linux, Windows, macOS), it could have come with Python already installed. And in that case it probably had some packages pre-installed with some specific versions **needed by your system**. If you install packages in the global Python environment, you could end up **breaking** some of the programs that came with your operating system. + +## Where are Packages Installed + +When you install Python, it creates some directories with some files in your computer. + +Some of these directories are the ones in charge of having all the packages you install. + +When you run: + +
+ +```console +// Don't run this now, it's just an example 🤓 +$ pip install sqlmodel +---> 100% +``` + +
+ +That will download a compressed file with the SQLModel code, normally from PyPI. + +It will also **download** files for other packages that SQLModel depends on. + +Then it will **extract** all those files and put them in a directory in your computer. + +By default, it will put those files downloaded and extracted in the directory that comes with your Python installation, that's the **global environment**. + +## What are Virtual Environments + +The solution to the problems of having all the packages in the global environment is to use a **virtual environment for each project** you work on. + +A virtual environment is a **directory**, very similar to the global one, where you can install the packages for a project. + +This way, each project will have its own virtual environment (`.venv` directory) with its own packages. + +```mermaid +flowchart TB + subgraph stone-project[philosophers-stone project] + stone(philosophers-stone) --->|requires| harry-1 + subgraph venv1[.venv] + harry-1[harry v1] + end + end + subgraph azkaban-project[prisoner-of-azkaban project] + azkaban(prisoner-of-azkaban) --->|requires| harry-3 + subgraph venv2[.venv] + harry-3[harry v3] + end + end + stone-project ~~~ azkaban-project +``` + +## What Does Activating a Virtual Environment Mean + +When you activate a virtual environment, for example with: + +//// tab | Linux, macOS + +
+ +```console +$ source .venv/bin/activate +``` + +
+ +//// + +//// tab | Windows PowerShell + +
+ +```console +$ .venv\Scripts\Activate.ps1 +``` + +
+ +//// + +//// tab | Windows Bash + +Or if you use Bash for Windows (e.g. Git Bash): + +
+ +```console +$ source .venv/Scripts/activate +``` + +
+ +//// + +That command will create or modify some [environment variables](environment-variables.md){.internal-link target=_blank} that will be available for the next commands. + +One of those variables is the `PATH` variable. + +/// tip + +You can learn more about the `PATH` environment variable in the [Environment Variables](environment-variables.md#path-environment-variable){.internal-link target=_blank} section. + +/// + +Activating a virtual environment adds its path `.venv/bin` (on Linux and macOS) or `.venv\Scripts` (on Windows) to the `PATH` environment variable. + +Let's say that before activating the environment, the `PATH` variable looked like this: + +//// tab | Linux, macOS + +```plaintext +/usr/bin:/bin:/usr/sbin:/sbin +``` + +That means that the system would look for programs in: + +* `/usr/bin` +* `/bin` +* `/usr/sbin` +* `/sbin` + +//// + +//// tab | Windows + +```plaintext +C:\Windows\System32 +``` + +That means that the system would look for programs in: + +* `C:\Windows\System32` + +//// + +After activating the virtual environment, the `PATH` variable would look something like this: + +//// tab | Linux, macOS + +```plaintext +/home/user/code/awesome-project/.venv/bin:/usr/bin:/bin:/usr/sbin:/sbin +``` + +That means that the system will now start looking first for programs in: + +```plaintext +/home/user/code/awesome-project/.venv/bin +``` + +before looking in the other directories. + +So, when you type `python` in the terminal, the system will find the Python program in + +```plaintext +/home/user/code/awesome-project/.venv/bin/python +``` + +and use that one. + +//// + +//// tab | Windows + +```plaintext +C:\Users\user\code\awesome-project\.venv\Scripts;C:\Windows\System32 +``` + +That means that the system will now start looking first look for programs in: + +```plaintext +C:\Users\user\code\awesome-project\.venv\Scripts +``` + +before looking in the other directories. + +So, when you type `python` in the terminal, the system will find the Python program in + +```plaintext +C:\Users\user\code\awesome-project\.venv\Scripts\python +``` + +and use that one. + +//// + +An important detail is that it will put the virtual environment path at the **beginning** of the `PATH` variable. The system will find it **before** finding any other Python available. This way, when you run `python`, it will use the Python **from the virtual environment** instead of any other `python` (for example, a `python` from a global environment). + +Activating a virtual environment also changes a couple of other things, but this is one of the most important things it does. + +## Checking a Virtual Environment + +When you check if a virtual environment is active, for example with: + +//// tab | Linux, macOS, Windows Bash + +
+ +```console +$ which python + +/home/user/code/awesome-project/.venv/bin/python +``` + +
+ +//// + +//// tab | Windows PowerShell + +
+ +```console +$ Get-Command python + +C:\Users\user\code\awesome-project\.venv\Scripts\python +``` + +
+ +//// + +That means that the `python` program that will be used is the one **in the virtual environment**. + +you use `which` in Linux and macOS and `Get-Command` in Windows PowerShell. + +The way that command works is that it will go and check in the `PATH` environment variable, going through **each path in order**, looking for the program called `python`. Once it finds it, it will **show you the path** to that program. + +The most important part is that when you call `python`, that is the exact "`python`" that will be executed. + +So, you can confirm if you are in the correct virtual environment. + +/// tip + +It's easy to activate one virtual environment, get one Python, and then **go to another project**. + +And the second project **wouldn't work** because you are using the **incorrect Python**, from a virtual environment for another project. + +It's useful being able to check what `python` is being used. 🤓 + +/// + +## Why Deactivate a Virtual Environment + +For example, you could be working on a project `philosophers-stone`, **activate that virtual environment**, install packages and work with that environment. + +And then you want to work on **another project** `prisoner-of-azkaban`. + +You go to that project: + +
+ +```console +$ cd ~/code/prisoner-of-azkaban +``` + +
+ +If you don't deactivate the virtual environment for `philosophers-stone`, when you run `python` in the terminal, it will try to use the Python from `philosophers-stone`. + +
+ +```console +$ cd ~/code/prisoner-of-azkaban + +$ python main.py + +// Error importing sirius, it's not installed 😱 +Traceback (most recent call last): + File "main.py", line 1, in + import sirius +``` + +
+ +But if you deactivate the virtual environment and activate the new one for `prisoner-of-askaban` then when you run `python` it will use the Python from the virtual environment in `prisoner-of-azkaban`. + +
+ +```console +$ cd ~/code/prisoner-of-azkaban + +// You don't need to be in the old directory to deactivate, you can do it wherever you are, even after going to the other project 😎 +$ deactivate + +// Activate the virtual environment in prisoner-of-azkaban/.venv 🚀 +$ source .venv/bin/activate + +// Now when you run python, it will find the package sirius installed in this virtual environment ✨ +$ python main.py + +I solemnly swear 🐺 +``` + +
+ +## Alternatives + +This is a simple guide to get you started and teach you how everything works **underneath**. + +There are many **alternatives** to managing virtual environments, package dependencies (requirements), projects. + +Once you are ready and want to use a tool to **manage the entire project**, packages dependencies, virtual environments, etc. I would suggest you try uv. + +`uv` can do a lot of things, it can: + +* **Install Python** for you, including different versions +* Manage the **virtual environment** for your projects +* Install **packages** +* Manage package **dependencies and versions** for your project +* Make sure you have an **exact** set of packages and versions to install, including their dependencies, so that you can be sure that you can run your project in production exactly the same as in your computer while developing, this is called **locking** +* And many other things + +## Conclusion + +If you read and understood all this, now **you know much more** about virtual environments than many developers out there. 🤓 + +Knowing these details will most probably be useful in a future time when you are debugging something that seems complex, but you will know **how it all works underneath**. 😎 diff --git a/docs_src/advanced/decimal/tutorial001_py310.py b/docs_src/advanced/decimal/tutorial001_py310.py new file mode 100644 index 0000000000..267338912e --- /dev/null +++ b/docs_src/advanced/decimal/tutorial001_py310.py @@ -0,0 +1,60 @@ +from decimal import Decimal + +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + money: Decimal = Field(default=0, max_digits=5, decimal_places=3) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson", money=1.1) + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador", money=0.001) + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48, money=2.2) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Deadpond") + results = session.exec(statement) + hero_1 = results.one() + print("Hero 1:", hero_1) + + statement = select(Hero).where(Hero.name == "Rusty-Man") + results = session.exec(statement) + hero_2 = results.one() + print("Hero 2:", hero_2) + + total_money = hero_1.money + hero_2.money + print(f"Total money: {total_money}") + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/advanced/decimal/tutorial001.py b/docs_src/advanced/decimal/tutorial001_py39.py similarity index 93% rename from docs_src/advanced/decimal/tutorial001.py rename to docs_src/advanced/decimal/tutorial001_py39.py index b803119d9e..a0a9804ade 100644 --- a/docs_src/advanced/decimal/tutorial001.py +++ b/docs_src/advanced/decimal/tutorial001_py39.py @@ -1,6 +1,6 @@ +from decimal import Decimal from typing import Optional -from pydantic import condecimal from sqlmodel import Field, Session, SQLModel, create_engine, select @@ -9,7 +9,7 @@ class Hero(SQLModel, table=True): name: str = Field(index=True) secret_name: str age: Optional[int] = Field(default=None, index=True) - money: condecimal(max_digits=5, decimal_places=3) = Field(default=0) + money: Decimal = Field(default=0, max_digits=5, decimal_places=3) sqlite_file_name = "database.db" diff --git a/docs_src/tutorial/code_structure/tutorial001/__init__.py b/docs_src/advanced/uuid/__init__.py similarity index 100% rename from docs_src/tutorial/code_structure/tutorial001/__init__.py rename to docs_src/advanced/uuid/__init__.py diff --git a/docs_src/advanced/uuid/tutorial001_py310.py b/docs_src/advanced/uuid/tutorial001_py310.py new file mode 100644 index 0000000000..610ec6b0d4 --- /dev/null +++ b/docs_src/advanced/uuid/tutorial001_py310.py @@ -0,0 +1,64 @@ +import uuid + +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_hero(): + with Session(engine) as session: + hero = Hero(name="Deadpond", secret_name="Dive Wilson") + print("The hero before saving in the DB") + print(hero) + print("The hero ID was already set") + print(hero.id) + session.add(hero) + session.commit() + session.refresh(hero) + print("After saving in the DB") + print(hero) + + +def select_hero(): + with Session(engine) as session: + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_2) + session.commit() + session.refresh(hero_2) + hero_id = hero_2.id + print("Created hero:") + print(hero_2) + print("Created hero ID:") + print(hero_id) + + statement = select(Hero).where(Hero.id == hero_id) + selected_hero = session.exec(statement).one() + print("Selected hero:") + print(selected_hero) + print("Selected hero ID:") + print(selected_hero.id) + + +def main() -> None: + create_db_and_tables() + create_hero() + select_hero() + + +if __name__ == "__main__": + main() diff --git a/docs_src/advanced/uuid/tutorial001_py39.py b/docs_src/advanced/uuid/tutorial001_py39.py new file mode 100644 index 0000000000..cfd3146b41 --- /dev/null +++ b/docs_src/advanced/uuid/tutorial001_py39.py @@ -0,0 +1,65 @@ +import uuid +from typing import Union + +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Union[int, None] = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_hero(): + with Session(engine) as session: + hero = Hero(name="Deadpond", secret_name="Dive Wilson") + print("The hero before saving in the DB") + print(hero) + print("The hero ID was already set") + print(hero.id) + session.add(hero) + session.commit() + session.refresh(hero) + print("After saving in the DB") + print(hero) + + +def select_hero(): + with Session(engine) as session: + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_2) + session.commit() + session.refresh(hero_2) + hero_id = hero_2.id + print("Created hero:") + print(hero_2) + print("Created hero ID:") + print(hero_id) + + statement = select(Hero).where(Hero.id == hero_id) + selected_hero = session.exec(statement).one() + print("Selected hero:") + print(selected_hero) + print("Selected hero ID:") + print(selected_hero.id) + + +def main() -> None: + create_db_and_tables() + create_hero() + select_hero() + + +if __name__ == "__main__": + main() diff --git a/docs_src/advanced/uuid/tutorial002_py310.py b/docs_src/advanced/uuid/tutorial002_py310.py new file mode 100644 index 0000000000..3ec8c80fa0 --- /dev/null +++ b/docs_src/advanced/uuid/tutorial002_py310.py @@ -0,0 +1,63 @@ +import uuid + +from sqlmodel import Field, Session, SQLModel, create_engine + + +class Hero(SQLModel, table=True): + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_hero(): + with Session(engine) as session: + hero = Hero(name="Deadpond", secret_name="Dive Wilson") + print("The hero before saving in the DB") + print(hero) + print("The hero ID was already set") + print(hero.id) + session.add(hero) + session.commit() + session.refresh(hero) + print("After saving in the DB") + print(hero) + + +def select_hero(): + with Session(engine) as session: + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_2) + session.commit() + session.refresh(hero_2) + hero_id = hero_2.id + print("Created hero:") + print(hero_2) + print("Created hero ID:") + print(hero_id) + + selected_hero = session.get(Hero, hero_id) + print("Selected hero:") + print(selected_hero) + print("Selected hero ID:") + print(selected_hero.id) + + +def main() -> None: + create_db_and_tables() + create_hero() + select_hero() + + +if __name__ == "__main__": + main() diff --git a/docs_src/advanced/uuid/tutorial002_py39.py b/docs_src/advanced/uuid/tutorial002_py39.py new file mode 100644 index 0000000000..831725581b --- /dev/null +++ b/docs_src/advanced/uuid/tutorial002_py39.py @@ -0,0 +1,64 @@ +import uuid +from typing import Union + +from sqlmodel import Field, Session, SQLModel, create_engine + + +class Hero(SQLModel, table=True): + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Union[int, None] = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_hero(): + with Session(engine) as session: + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + print("The hero before saving in the DB") + print(hero_1) + print("The hero ID was already set") + print(hero_1.id) + session.add(hero_1) + session.commit() + session.refresh(hero_1) + print("After saving in the DB") + print(hero_1) + + +def select_hero(): + with Session(engine) as session: + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_2) + session.commit() + session.refresh(hero_2) + hero_id = hero_2.id + print("Created hero:") + print(hero_2) + print("Created hero ID:") + print(hero_id) + + selected_hero = session.get(Hero, hero_id) + print("Selected hero:") + print(selected_hero) + print("Selected hero ID:") + print(selected_hero.id) + + +def main() -> None: + create_db_and_tables() + create_hero() + select_hero() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/automatic_id_none_refresh/annotations/en/tutorial002.md b/docs_src/tutorial/automatic_id_none_refresh/annotations/en/tutorial002.md index fd33fec778..fee38368dc 100644 --- a/docs_src/tutorial/automatic_id_none_refresh/annotations/en/tutorial002.md +++ b/docs_src/tutorial/automatic_id_none_refresh/annotations/en/tutorial002.md @@ -157,7 +157,7 @@ Hero 3: ``` -21. Print the line `"After commiting the session, show IDs"`. +21. Print the line `"After committing the session, show IDs"`. Generates the output: @@ -181,8 +181,8 @@ ``` INFO Engine BEGIN (implicit) - INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age - FROM hero + INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age + FROM hero WHERE hero.id = ? INFO Engine [generated in 0.00017s] (1,) @@ -196,8 +196,8 @@ Generates the output: ``` - INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age - FROM hero + INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age + FROM hero WHERE hero.id = ? INFO Engine [cached since 0.001245s ago] (2,) @@ -211,8 +211,8 @@ Generates the output: ``` - INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age - FROM hero + INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age + FROM hero WHERE hero.id = ? INFO Engine [cached since 0.002215s ago] (3,) @@ -265,8 +265,8 @@ Generates the output: ``` - INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age - FROM hero + INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age + FROM hero WHERE hero.id = ? INFO Engine [generated in 0.00024s] (1,) ``` @@ -278,8 +278,8 @@ Generates the output: ``` - INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age - FROM hero + INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age + FROM hero WHERE hero.id = ? INFO Engine [cached since 0.001487s ago] (2,) ``` @@ -291,8 +291,8 @@ Generates the output: ``` - INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age - FROM hero + INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age + FROM hero WHERE hero.id = ? INFO Engine [cached since 0.002377s ago] (3,) ``` @@ -307,8 +307,11 @@ 33. Print the `hero_1`. - !!! info - Even if the `hero_1` wasn't fresh, this would **not** trigger a `refresh` making the **session** use the **engine** to fetch data from the database because it is not accessing an attribute. + /// info + + Even if the `hero_1` wasn't fresh, this would **not** trigger a `refresh` making the **session** use the **engine** to fetch data from the database because it is not accessing an attribute. + + /// Because the `hero_1` is fresh it has all it's data available. @@ -320,8 +323,11 @@ 34. Print the `hero_2`. - !!! info - Even if the `hero_2` wasn't fresh, this would **not** trigger a `refresh` making the **session** use the **engine** to fetch data from the database because it is not accessing an attribute. + /// info + + Even if the `hero_2` wasn't fresh, this would **not** trigger a `refresh` making the **session** use the **engine** to fetch data from the database because it is not accessing an attribute. + + /// Because the `hero_2` is fresh it has all it's data available. @@ -333,8 +339,11 @@ 35. Print the `hero_3`. - !!! info - Even if the `hero_3` wasn't fresh, this would **not** trigger a `refresh` making the **session** use the **engine** to fetch data from the database because it is not accessing an attribute. + /// info + + Even if the `hero_3` wasn't fresh, this would **not** trigger a `refresh` making the **session** use the **engine** to fetch data from the database because it is not accessing an attribute. + + /// Because the `hero_3` is fresh it has all it's data available. diff --git a/docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py b/docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py new file mode 100644 index 0000000000..6b76da58ff --- /dev/null +++ b/docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py @@ -0,0 +1,79 @@ +from sqlmodel import Field, Session, SQLModel, create_engine + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + + print("Before interacting with the database") + print("Hero 1:", hero_1) + print("Hero 2:", hero_2) + print("Hero 3:", hero_3) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + + print("After adding to the session") + print("Hero 1:", hero_1) + print("Hero 2:", hero_2) + print("Hero 3:", hero_3) + + session.commit() + + print("After committing the session") + print("Hero 1:", hero_1) + print("Hero 2:", hero_2) + print("Hero 3:", hero_3) + + print("After committing the session, show IDs") + print("Hero 1 ID:", hero_1.id) + print("Hero 2 ID:", hero_2.id) + print("Hero 3 ID:", hero_3.id) + + print("After committing the session, show names") + print("Hero 1 name:", hero_1.name) + print("Hero 2 name:", hero_2.name) + print("Hero 3 name:", hero_3.name) + + session.refresh(hero_1) + session.refresh(hero_2) + session.refresh(hero_3) + + print("After refreshing the heroes") + print("Hero 1:", hero_1) + print("Hero 2:", hero_2) + print("Hero 3:", hero_3) + + print("After the session closes") + print("Hero 1:", hero_1) + print("Hero 2:", hero_2) + print("Hero 3:", hero_3) + + +def main(): + create_db_and_tables() + create_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/automatic_id_none_refresh/tutorial001.py b/docs_src/tutorial/automatic_id_none_refresh/tutorial001_py39.py similarity index 100% rename from docs_src/tutorial/automatic_id_none_refresh/tutorial001.py rename to docs_src/tutorial/automatic_id_none_refresh/tutorial001_py39.py diff --git a/docs_src/tutorial/automatic_id_none_refresh/tutorial002.py b/docs_src/tutorial/automatic_id_none_refresh/tutorial002.py deleted file mode 100644 index 1c7cd53e2f..0000000000 --- a/docs_src/tutorial/automatic_id_none_refresh/tutorial002.py +++ /dev/null @@ -1,82 +0,0 @@ -from typing import Optional - -from sqlmodel import Field, Session, SQLModel, create_engine - - -class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str - secret_name: str - age: Optional[int] = None - - -sqlite_file_name = "database.db" -sqlite_url = f"sqlite:///{sqlite_file_name}" - -engine = create_engine(sqlite_url, echo=True) - - -def create_db_and_tables(): - SQLModel.metadata.create_all(engine) - - -def create_heroes(): - hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") # (1) - hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") # (2) - hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) # (3) - - print("Before interacting with the database") # (4) - print("Hero 1:", hero_1) # (5) - print("Hero 2:", hero_2) # (6) - print("Hero 3:", hero_3) # (7) - - with Session(engine) as session: # (8) - session.add(hero_1) # (9) - session.add(hero_2) # (10) - session.add(hero_3) # (11) - - print("After adding to the session") # (12) - print("Hero 1:", hero_1) # (13) - print("Hero 2:", hero_2) # (14) - print("Hero 3:", hero_3) # (15) - - session.commit() # (16) - - print("After committing the session") # (17) - print("Hero 1:", hero_1) # (18) - print("Hero 2:", hero_2) # (19) - print("Hero 3:", hero_3) # (20) - - print("After committing the session, show IDs") # (21) - print("Hero 1 ID:", hero_1.id) # (22) - print("Hero 2 ID:", hero_2.id) # (23) - print("Hero 3 ID:", hero_3.id) # (24) - - print("After committing the session, show names") # (25) - print("Hero 1 name:", hero_1.name) # (26) - print("Hero 2 name:", hero_2.name) # (27) - print("Hero 3 name:", hero_3.name) # (28) - - session.refresh(hero_1) # (29) - session.refresh(hero_2) # (30) - session.refresh(hero_3) # (31) - - print("After refreshing the heroes") # (32) - print("Hero 1:", hero_1) # (33) - print("Hero 2:", hero_2) # (34) - print("Hero 3:", hero_3) # (35) - # (36) - - print("After the session closes") # (37) - print("Hero 1:", hero_1) # (38) - print("Hero 2:", hero_2) # (39) - print("Hero 3:", hero_3) # (40) - - -def main(): - create_db_and_tables() - create_heroes() - - -if __name__ == "__main__": - main() diff --git a/docs_src/tutorial/automatic_id_none_refresh/tutorial002_py310.py b/docs_src/tutorial/automatic_id_none_refresh/tutorial002_py310.py new file mode 100644 index 0000000000..0f3ad44cf1 --- /dev/null +++ b/docs_src/tutorial/automatic_id_none_refresh/tutorial002_py310.py @@ -0,0 +1,80 @@ +from sqlmodel import Field, Session, SQLModel, create_engine + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") # (1)! + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") # (2)! + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) # (3)! + + print("Before interacting with the database") # (4)! + print("Hero 1:", hero_1) # (5)! + print("Hero 2:", hero_2) # (6)! + print("Hero 3:", hero_3) # (7)! + + with Session(engine) as session: # (8)! + session.add(hero_1) # (9)! + session.add(hero_2) # (10)! + session.add(hero_3) # (11)! + + print("After adding to the session") # (12)! + print("Hero 1:", hero_1) # (13)! + print("Hero 2:", hero_2) # (14)! + print("Hero 3:", hero_3) # (15)! + + session.commit() # (16)! + + print("After committing the session") # (17)! + print("Hero 1:", hero_1) # (18)! + print("Hero 2:", hero_2) # (19)! + print("Hero 3:", hero_3) # (20)! + + print("After committing the session, show IDs") # (21)! + print("Hero 1 ID:", hero_1.id) # (22)! + print("Hero 2 ID:", hero_2.id) # (23)! + print("Hero 3 ID:", hero_3.id) # (24)! + + print("After committing the session, show names") # (25)! + print("Hero 1 name:", hero_1.name) # (26)! + print("Hero 2 name:", hero_2.name) # (27)! + print("Hero 3 name:", hero_3.name) # (28)! + + session.refresh(hero_1) # (29)! + session.refresh(hero_2) # (30)! + session.refresh(hero_3) # (31)! + + print("After refreshing the heroes") # (32)! + print("Hero 1:", hero_1) # (33)! + print("Hero 2:", hero_2) # (34)! + print("Hero 3:", hero_3) # (35)! + # (36)! + + print("After the session closes") # (37)! + print("Hero 1:", hero_1) # (38)! + print("Hero 2:", hero_2) # (39)! + print("Hero 3:", hero_3) # (40)! + + +def main(): + create_db_and_tables() + create_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/automatic_id_none_refresh/tutorial002_py39.py b/docs_src/tutorial/automatic_id_none_refresh/tutorial002_py39.py new file mode 100644 index 0000000000..c597506a62 --- /dev/null +++ b/docs_src/tutorial/automatic_id_none_refresh/tutorial002_py39.py @@ -0,0 +1,82 @@ +from typing import Optional + +from sqlmodel import Field, Session, SQLModel, create_engine + + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str + secret_name: str + age: Optional[int] = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") # (1)! + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") # (2)! + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) # (3)! + + print("Before interacting with the database") # (4)! + print("Hero 1:", hero_1) # (5)! + print("Hero 2:", hero_2) # (6)! + print("Hero 3:", hero_3) # (7)! + + with Session(engine) as session: # (8)! + session.add(hero_1) # (9)! + session.add(hero_2) # (10)! + session.add(hero_3) # (11)! + + print("After adding to the session") # (12)! + print("Hero 1:", hero_1) # (13)! + print("Hero 2:", hero_2) # (14)! + print("Hero 3:", hero_3) # (15)! + + session.commit() # (16)! + + print("After committing the session") # (17)! + print("Hero 1:", hero_1) # (18)! + print("Hero 2:", hero_2) # (19)! + print("Hero 3:", hero_3) # (20)! + + print("After committing the session, show IDs") # (21)! + print("Hero 1 ID:", hero_1.id) # (22)! + print("Hero 2 ID:", hero_2.id) # (23)! + print("Hero 3 ID:", hero_3.id) # (24)! + + print("After committing the session, show names") # (25)! + print("Hero 1 name:", hero_1.name) # (26)! + print("Hero 2 name:", hero_2.name) # (27)! + print("Hero 3 name:", hero_3.name) # (28)! + + session.refresh(hero_1) # (29)! + session.refresh(hero_2) # (30)! + session.refresh(hero_3) # (31)! + + print("After refreshing the heroes") # (32)! + print("Hero 1:", hero_1) # (33)! + print("Hero 2:", hero_2) # (34)! + print("Hero 3:", hero_3) # (35)! + # (36)! + + print("After the session closes") # (37)! + print("Hero 1:", hero_1) # (38)! + print("Hero 2:", hero_2) # (39)! + print("Hero 3:", hero_3) # (40)! + + +def main(): + create_db_and_tables() + create_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/code_structure/tutorial002/__init__.py b/docs_src/tutorial/code_structure/tutorial001_py310/__init__.py similarity index 100% rename from docs_src/tutorial/code_structure/tutorial002/__init__.py rename to docs_src/tutorial/code_structure/tutorial001_py310/__init__.py diff --git a/docs_src/tutorial/code_structure/tutorial001/app.py b/docs_src/tutorial/code_structure/tutorial001_py310/app.py similarity index 97% rename from docs_src/tutorial/code_structure/tutorial001/app.py rename to docs_src/tutorial/code_structure/tutorial001_py310/app.py index 065f8a78b5..3d1bfc69a0 100644 --- a/docs_src/tutorial/code_structure/tutorial001/app.py +++ b/docs_src/tutorial/code_structure/tutorial001_py310/app.py @@ -6,7 +6,7 @@ def create_heroes(): with Session(engine) as session: - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", secret_name="Dive Wilson", team=team_z_force diff --git a/docs_src/tutorial/code_structure/tutorial001/database.py b/docs_src/tutorial/code_structure/tutorial001_py310/database.py similarity index 100% rename from docs_src/tutorial/code_structure/tutorial001/database.py rename to docs_src/tutorial/code_structure/tutorial001_py310/database.py diff --git a/docs_src/tutorial/code_structure/tutorial001_py310/models.py b/docs_src/tutorial/code_structure/tutorial001_py310/models.py new file mode 100644 index 0000000000..1f485ef0b1 --- /dev/null +++ b/docs_src/tutorial/code_structure/tutorial001_py310/models.py @@ -0,0 +1,19 @@ +from sqlmodel import Field, Relationship, SQLModel + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team") + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id") + team: Team | None = Relationship(back_populates="heroes") diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/__init__.py b/docs_src/tutorial/code_structure/tutorial001_py39/__init__.py similarity index 100% rename from docs_src/tutorial/fastapi/app_testing/tutorial001/__init__.py rename to docs_src/tutorial/code_structure/tutorial001_py39/__init__.py diff --git a/docs_src/tutorial/code_structure/tutorial001_py39/app.py b/docs_src/tutorial/code_structure/tutorial001_py39/app.py new file mode 100644 index 0000000000..3d1bfc69a0 --- /dev/null +++ b/docs_src/tutorial/code_structure/tutorial001_py39/app.py @@ -0,0 +1,29 @@ +from sqlmodel import Session + +from .database import create_db_and_tables, engine +from .models import Hero, Team + + +def create_heroes(): + with Session(engine) as session: + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + session.add(hero_deadpond) + session.commit() + + session.refresh(hero_deadpond) + + print("Created hero:", hero_deadpond) + print("Hero's team:", hero_deadpond.team) + + +def main(): + create_db_and_tables() + create_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/code_structure/tutorial002/database.py b/docs_src/tutorial/code_structure/tutorial001_py39/database.py similarity index 100% rename from docs_src/tutorial/code_structure/tutorial002/database.py rename to docs_src/tutorial/code_structure/tutorial001_py39/database.py diff --git a/docs_src/tutorial/code_structure/tutorial001/models.py b/docs_src/tutorial/code_structure/tutorial001_py39/models.py similarity index 85% rename from docs_src/tutorial/code_structure/tutorial001/models.py rename to docs_src/tutorial/code_structure/tutorial001_py39/models.py index 8e2647b3c4..ff6b6c2d66 100644 --- a/docs_src/tutorial/code_structure/tutorial001/models.py +++ b/docs_src/tutorial/code_structure/tutorial001_py39/models.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Optional from sqlmodel import Field, Relationship, SQLModel @@ -8,7 +8,7 @@ class Team(SQLModel, table=True): name: str = Field(index=True) headquarters: str - heroes: List["Hero"] = Relationship(back_populates="team") + heroes: list["Hero"] = Relationship(back_populates="team") class Hero(SQLModel, table=True): diff --git a/sqlmodel/engine/__init__.py b/docs_src/tutorial/code_structure/tutorial002_py310/__init__.py similarity index 100% rename from sqlmodel/engine/__init__.py rename to docs_src/tutorial/code_structure/tutorial002_py310/__init__.py diff --git a/docs_src/tutorial/code_structure/tutorial002/app.py b/docs_src/tutorial/code_structure/tutorial002_py310/app.py similarity index 97% rename from docs_src/tutorial/code_structure/tutorial002/app.py rename to docs_src/tutorial/code_structure/tutorial002_py310/app.py index 8afaee7c16..2ecaec0c3c 100644 --- a/docs_src/tutorial/code_structure/tutorial002/app.py +++ b/docs_src/tutorial/code_structure/tutorial002_py310/app.py @@ -7,7 +7,7 @@ def create_heroes(): with Session(engine) as session: - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", secret_name="Dive Wilson", team=team_z_force diff --git a/docs_src/tutorial/code_structure/tutorial002_py310/database.py b/docs_src/tutorial/code_structure/tutorial002_py310/database.py new file mode 100644 index 0000000000..d6de16c11f --- /dev/null +++ b/docs_src/tutorial/code_structure/tutorial002_py310/database.py @@ -0,0 +1,10 @@ +from sqlmodel import SQLModel, create_engine + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) diff --git a/docs_src/tutorial/code_structure/tutorial002_py310/hero_model.py b/docs_src/tutorial/code_structure/tutorial002_py310/hero_model.py new file mode 100644 index 0000000000..52fe68be16 --- /dev/null +++ b/docs_src/tutorial/code_structure/tutorial002_py310/hero_model.py @@ -0,0 +1,16 @@ +from typing import TYPE_CHECKING, Optional + +from sqlmodel import Field, Relationship, SQLModel + +if TYPE_CHECKING: + from .team_model import Team + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id") + team: Optional["Team"] = Relationship(back_populates="heroes") diff --git a/docs_src/tutorial/code_structure/tutorial002_py310/team_model.py b/docs_src/tutorial/code_structure/tutorial002_py310/team_model.py new file mode 100644 index 0000000000..10af5b9c2f --- /dev/null +++ b/docs_src/tutorial/code_structure/tutorial002_py310/team_model.py @@ -0,0 +1,14 @@ +from typing import TYPE_CHECKING + +from sqlmodel import Field, Relationship, SQLModel + +if TYPE_CHECKING: + from .hero_model import Hero + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team") diff --git a/docs_src/tutorial/code_structure/tutorial002_py39/__init__.py b/docs_src/tutorial/code_structure/tutorial002_py39/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/tutorial/code_structure/tutorial002_py39/app.py b/docs_src/tutorial/code_structure/tutorial002_py39/app.py new file mode 100644 index 0000000000..2ecaec0c3c --- /dev/null +++ b/docs_src/tutorial/code_structure/tutorial002_py39/app.py @@ -0,0 +1,30 @@ +from sqlmodel import Session + +from .database import create_db_and_tables, engine +from .hero_model import Hero +from .team_model import Team + + +def create_heroes(): + with Session(engine) as session: + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + session.add(hero_deadpond) + session.commit() + + session.refresh(hero_deadpond) + + print("Created hero:", hero_deadpond) + print("Hero's team:", hero_deadpond.team) + + +def main(): + create_db_and_tables() + create_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/code_structure/tutorial002_py39/database.py b/docs_src/tutorial/code_structure/tutorial002_py39/database.py new file mode 100644 index 0000000000..d6de16c11f --- /dev/null +++ b/docs_src/tutorial/code_structure/tutorial002_py39/database.py @@ -0,0 +1,10 @@ +from sqlmodel import SQLModel, create_engine + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) diff --git a/docs_src/tutorial/code_structure/tutorial002/hero_model.py b/docs_src/tutorial/code_structure/tutorial002_py39/hero_model.py similarity index 100% rename from docs_src/tutorial/code_structure/tutorial002/hero_model.py rename to docs_src/tutorial/code_structure/tutorial002_py39/hero_model.py diff --git a/docs_src/tutorial/code_structure/tutorial002/team_model.py b/docs_src/tutorial/code_structure/tutorial002_py39/team_model.py similarity index 69% rename from docs_src/tutorial/code_structure/tutorial002/team_model.py rename to docs_src/tutorial/code_structure/tutorial002_py39/team_model.py index c8a008bf4c..b51c070cf1 100644 --- a/docs_src/tutorial/code_structure/tutorial002/team_model.py +++ b/docs_src/tutorial/code_structure/tutorial002_py39/team_model.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, List, Optional +from typing import TYPE_CHECKING, Optional from sqlmodel import Field, Relationship, SQLModel @@ -11,4 +11,4 @@ class Team(SQLModel, table=True): name: str = Field(index=True) headquarters: str - heroes: List["Hero"] = Relationship(back_populates="team") + heroes: list["Hero"] = Relationship(back_populates="team") diff --git a/docs_src/tutorial/connect/create_tables/tutorial001_py310.py b/docs_src/tutorial/connect/create_tables/tutorial001_py310.py new file mode 100644 index 0000000000..460b9768d7 --- /dev/null +++ b/docs_src/tutorial/connect/create_tables/tutorial001_py310.py @@ -0,0 +1,34 @@ +from sqlmodel import Field, SQLModel, create_engine + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def main(): + create_db_and_tables() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/connect/create_tables/tutorial001.py b/docs_src/tutorial/connect/create_tables/tutorial001_py39.py similarity index 100% rename from docs_src/tutorial/connect/create_tables/tutorial001.py rename to docs_src/tutorial/connect/create_tables/tutorial001_py39.py diff --git a/docs_src/tutorial/connect/delete/tutorial001_py310.py b/docs_src/tutorial/connect/delete/tutorial001_py310.py new file mode 100644 index 0000000000..de0dd8d7e7 --- /dev/null +++ b/docs_src/tutorial/connect/delete/tutorial001_py310.py @@ -0,0 +1,79 @@ +from sqlmodel import Field, Session, SQLModel, create_engine + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + session.add(team_preventers) + session.add(team_z_force) + session.commit() + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team_id=team_z_force.id + ) + hero_rusty_man = Hero( + name="Rusty-Man", + secret_name="Tommy Sharp", + age=48, + team_id=team_preventers.id, + ) + hero_spider_boy = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_deadpond) + session.add(hero_rusty_man) + session.add(hero_spider_boy) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + session.refresh(hero_spider_boy) + + print("Created hero:", hero_deadpond) + print("Created hero:", hero_rusty_man) + print("Created hero:", hero_spider_boy) + + hero_spider_boy.team_id = team_preventers.id + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_spider_boy.team_id = None + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("No longer Preventer:", hero_spider_boy) + + +def main(): + create_db_and_tables() + create_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/connect/delete/tutorial001.py b/docs_src/tutorial/connect/delete/tutorial001_py39.py similarity index 99% rename from docs_src/tutorial/connect/delete/tutorial001.py rename to docs_src/tutorial/connect/delete/tutorial001_py39.py index eeb376a0cc..aa7d0db287 100644 --- a/docs_src/tutorial/connect/delete/tutorial001.py +++ b/docs_src/tutorial/connect/delete/tutorial001_py39.py @@ -31,7 +31,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") session.add(team_preventers) session.add(team_z_force) session.commit() diff --git a/docs_src/tutorial/connect/insert/tutorial001_py310.py b/docs_src/tutorial/connect/insert/tutorial001_py310.py new file mode 100644 index 0000000000..16c2c7fe09 --- /dev/null +++ b/docs_src/tutorial/connect/insert/tutorial001_py310.py @@ -0,0 +1,67 @@ +from sqlmodel import Field, Session, SQLModel, create_engine + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + session.add(team_preventers) + session.add(team_z_force) + session.commit() + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team_id=team_z_force.id + ) + hero_rusty_man = Hero( + name="Rusty-Man", + secret_name="Tommy Sharp", + age=48, + team_id=team_preventers.id, + ) + hero_spider_boy = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_deadpond) + session.add(hero_rusty_man) + session.add(hero_spider_boy) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + session.refresh(hero_spider_boy) + + print("Created hero:", hero_deadpond) + print("Created hero:", hero_rusty_man) + print("Created hero:", hero_spider_boy) + + +def main(): + create_db_and_tables() + create_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/connect/insert/tutorial001.py b/docs_src/tutorial/connect/insert/tutorial001_py39.py similarity index 98% rename from docs_src/tutorial/connect/insert/tutorial001.py rename to docs_src/tutorial/connect/insert/tutorial001_py39.py index dc3661d7c7..d2e3b2f0e3 100644 --- a/docs_src/tutorial/connect/insert/tutorial001.py +++ b/docs_src/tutorial/connect/insert/tutorial001_py39.py @@ -31,7 +31,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") session.add(team_preventers) session.add(team_z_force) session.commit() diff --git a/docs_src/tutorial/connect/select/tutorial001_py310.py b/docs_src/tutorial/connect/select/tutorial001_py310.py new file mode 100644 index 0000000000..faa25b3891 --- /dev/null +++ b/docs_src/tutorial/connect/select/tutorial001_py310.py @@ -0,0 +1,76 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + session.add(team_preventers) + session.add(team_z_force) + session.commit() + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team_id=team_z_force.id + ) + hero_rusty_man = Hero( + name="Rusty-Man", + secret_name="Tommy Sharp", + age=48, + team_id=team_preventers.id, + ) + hero_spider_boy = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_deadpond) + session.add(hero_rusty_man) + session.add(hero_spider_boy) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + session.refresh(hero_spider_boy) + + print("Created hero:", hero_deadpond) + print("Created hero:", hero_rusty_man) + print("Created hero:", hero_spider_boy) + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero, Team).where(Hero.team_id == Team.id) + results = session.exec(statement) + for hero, team in results: + print("Hero:", hero, "Team:", team) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/connect/select/tutorial001.py b/docs_src/tutorial/connect/select/tutorial001_py39.py similarity index 99% rename from docs_src/tutorial/connect/select/tutorial001.py rename to docs_src/tutorial/connect/select/tutorial001_py39.py index d4cdf413f1..d98e635779 100644 --- a/docs_src/tutorial/connect/select/tutorial001.py +++ b/docs_src/tutorial/connect/select/tutorial001_py39.py @@ -31,7 +31,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") session.add(team_preventers) session.add(team_z_force) session.commit() diff --git a/docs_src/tutorial/connect/select/tutorial002_py310.py b/docs_src/tutorial/connect/select/tutorial002_py310.py new file mode 100644 index 0000000000..08adc69d32 --- /dev/null +++ b/docs_src/tutorial/connect/select/tutorial002_py310.py @@ -0,0 +1,76 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + session.add(team_preventers) + session.add(team_z_force) + session.commit() + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team_id=team_z_force.id + ) + hero_rusty_man = Hero( + name="Rusty-Man", + secret_name="Tommy Sharp", + age=48, + team_id=team_preventers.id, + ) + hero_spider_boy = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_deadpond) + session.add(hero_rusty_man) + session.add(hero_spider_boy) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + session.refresh(hero_spider_boy) + + print("Created hero:", hero_deadpond) + print("Created hero:", hero_rusty_man) + print("Created hero:", hero_spider_boy) + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero, Team).join(Team) + results = session.exec(statement) + for hero, team in results: + print("Hero:", hero, "Team:", team) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/connect/select/tutorial002.py b/docs_src/tutorial/connect/select/tutorial002_py39.py similarity index 99% rename from docs_src/tutorial/connect/select/tutorial002.py rename to docs_src/tutorial/connect/select/tutorial002_py39.py index 59edbf7fd9..270f95003d 100644 --- a/docs_src/tutorial/connect/select/tutorial002.py +++ b/docs_src/tutorial/connect/select/tutorial002_py39.py @@ -31,7 +31,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") session.add(team_preventers) session.add(team_z_force) session.commit() diff --git a/docs_src/tutorial/connect/select/tutorial003_py310.py b/docs_src/tutorial/connect/select/tutorial003_py310.py new file mode 100644 index 0000000000..07127c6901 --- /dev/null +++ b/docs_src/tutorial/connect/select/tutorial003_py310.py @@ -0,0 +1,76 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + session.add(team_preventers) + session.add(team_z_force) + session.commit() + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team_id=team_z_force.id + ) + hero_rusty_man = Hero( + name="Rusty-Man", + secret_name="Tommy Sharp", + age=48, + team_id=team_preventers.id, + ) + hero_spider_boy = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_deadpond) + session.add(hero_rusty_man) + session.add(hero_spider_boy) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + session.refresh(hero_spider_boy) + + print("Created hero:", hero_deadpond) + print("Created hero:", hero_rusty_man) + print("Created hero:", hero_spider_boy) + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero, Team).join(Team, isouter=True) + results = session.exec(statement) + for hero, team in results: + print("Hero:", hero, "Team:", team) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/connect/select/tutorial003.py b/docs_src/tutorial/connect/select/tutorial003_py39.py similarity index 99% rename from docs_src/tutorial/connect/select/tutorial003.py rename to docs_src/tutorial/connect/select/tutorial003_py39.py index fb5b8aa0c9..ee427e309c 100644 --- a/docs_src/tutorial/connect/select/tutorial003.py +++ b/docs_src/tutorial/connect/select/tutorial003_py39.py @@ -31,7 +31,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") session.add(team_preventers) session.add(team_z_force) session.commit() diff --git a/docs_src/tutorial/connect/select/tutorial004_py310.py b/docs_src/tutorial/connect/select/tutorial004_py310.py new file mode 100644 index 0000000000..6bcdee96be --- /dev/null +++ b/docs_src/tutorial/connect/select/tutorial004_py310.py @@ -0,0 +1,76 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + session.add(team_preventers) + session.add(team_z_force) + session.commit() + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team_id=team_z_force.id + ) + hero_rusty_man = Hero( + name="Rusty-Man", + secret_name="Tommy Sharp", + age=48, + team_id=team_preventers.id, + ) + hero_spider_boy = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_deadpond) + session.add(hero_rusty_man) + session.add(hero_spider_boy) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + session.refresh(hero_spider_boy) + + print("Created hero:", hero_deadpond) + print("Created hero:", hero_rusty_man) + print("Created hero:", hero_spider_boy) + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).join(Team).where(Team.name == "Preventers") + results = session.exec(statement) + for hero in results: + print("Preventer Hero:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/connect/select/tutorial004.py b/docs_src/tutorial/connect/select/tutorial004_py39.py similarity index 99% rename from docs_src/tutorial/connect/select/tutorial004.py rename to docs_src/tutorial/connect/select/tutorial004_py39.py index d1d260b3f4..29a7c205bd 100644 --- a/docs_src/tutorial/connect/select/tutorial004.py +++ b/docs_src/tutorial/connect/select/tutorial004_py39.py @@ -31,7 +31,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") session.add(team_preventers) session.add(team_z_force) session.commit() diff --git a/docs_src/tutorial/connect/select/tutorial005_py310.py b/docs_src/tutorial/connect/select/tutorial005_py310.py new file mode 100644 index 0000000000..445572a147 --- /dev/null +++ b/docs_src/tutorial/connect/select/tutorial005_py310.py @@ -0,0 +1,76 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + session.add(team_preventers) + session.add(team_z_force) + session.commit() + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team_id=team_z_force.id + ) + hero_rusty_man = Hero( + name="Rusty-Man", + secret_name="Tommy Sharp", + age=48, + team_id=team_preventers.id, + ) + hero_spider_boy = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_deadpond) + session.add(hero_rusty_man) + session.add(hero_spider_boy) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + session.refresh(hero_spider_boy) + + print("Created hero:", hero_deadpond) + print("Created hero:", hero_rusty_man) + print("Created hero:", hero_spider_boy) + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero, Team).join(Team).where(Team.name == "Preventers") + results = session.exec(statement) + for hero, team in results: + print("Preventer Hero:", hero, "Team:", team) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/connect/select/tutorial005.py b/docs_src/tutorial/connect/select/tutorial005_py39.py similarity index 99% rename from docs_src/tutorial/connect/select/tutorial005.py rename to docs_src/tutorial/connect/select/tutorial005_py39.py index a61ef8a015..96a12ab537 100644 --- a/docs_src/tutorial/connect/select/tutorial005.py +++ b/docs_src/tutorial/connect/select/tutorial005_py39.py @@ -31,7 +31,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") session.add(team_preventers) session.add(team_z_force) session.commit() diff --git a/docs_src/tutorial/connect/update/tutorial001_py310.py b/docs_src/tutorial/connect/update/tutorial001_py310.py new file mode 100644 index 0000000000..a6ebfa6ee0 --- /dev/null +++ b/docs_src/tutorial/connect/update/tutorial001_py310.py @@ -0,0 +1,73 @@ +from sqlmodel import Field, Session, SQLModel, create_engine + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + session.add(team_preventers) + session.add(team_z_force) + session.commit() + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team_id=team_z_force.id + ) + hero_rusty_man = Hero( + name="Rusty-Man", + secret_name="Tommy Sharp", + age=48, + team_id=team_preventers.id, + ) + hero_spider_boy = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_deadpond) + session.add(hero_rusty_man) + session.add(hero_spider_boy) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + session.refresh(hero_spider_boy) + + print("Created hero:", hero_deadpond) + print("Created hero:", hero_rusty_man) + print("Created hero:", hero_spider_boy) + + hero_spider_boy.team_id = team_preventers.id + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + +def main(): + create_db_and_tables() + create_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/connect/update/tutorial001.py b/docs_src/tutorial/connect/update/tutorial001_py39.py similarity index 99% rename from docs_src/tutorial/connect/update/tutorial001.py rename to docs_src/tutorial/connect/update/tutorial001_py39.py index 0080340532..b32599fc0c 100644 --- a/docs_src/tutorial/connect/update/tutorial001.py +++ b/docs_src/tutorial/connect/update/tutorial001_py39.py @@ -31,7 +31,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") session.add(team_preventers) session.add(team_z_force) session.commit() diff --git a/docs_src/tutorial/create_db_and_table/annotations/en/tutorial003.md b/docs_src/tutorial/create_db_and_table/annotations/en/tutorial003.md index f20779a57c..81b548b50c 100644 --- a/docs_src/tutorial/create_db_and_table/annotations/en/tutorial003.md +++ b/docs_src/tutorial/create_db_and_table/annotations/en/tutorial003.md @@ -6,7 +6,7 @@ 4. Create the `id` field: - It could be `None` until the database assigns a value to it, so we annotate it with `Optional`. + It could be `None` until the database assigns a value to it, so we annotate it with `Optional` (`int | None` in Python 3.10+). It is a **primary key**, so we use `Field()` and the argument `primary_key=True`. @@ -24,7 +24,7 @@ In the database, the default value will be `NULL`, the SQL equivalent of `None`. - As this field could be `None` (and `NULL` in the database), we annotate it with `Optional`. + As this field could be `None` (and `NULL` in the database), we annotate it with `Optional` (`int | None` in Python 3.10+). 8. Write the name of the database file. 9. Use the name of the database file to create the database URL. diff --git a/docs_src/tutorial/create_db_and_table/tutorial001_py310.py b/docs_src/tutorial/create_db_and_table/tutorial001_py310.py new file mode 100644 index 0000000000..13ae331bf1 --- /dev/null +++ b/docs_src/tutorial/create_db_and_table/tutorial001_py310.py @@ -0,0 +1,16 @@ +from sqlmodel import Field, SQLModel, create_engine + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + +SQLModel.metadata.create_all(engine) diff --git a/docs_src/tutorial/create_db_and_table/tutorial001.py b/docs_src/tutorial/create_db_and_table/tutorial001_py39.py similarity index 100% rename from docs_src/tutorial/create_db_and_table/tutorial001.py rename to docs_src/tutorial/create_db_and_table/tutorial001_py39.py diff --git a/docs_src/tutorial/create_db_and_table/tutorial002_py310.py b/docs_src/tutorial/create_db_and_table/tutorial002_py310.py new file mode 100644 index 0000000000..50c7241ef4 --- /dev/null +++ b/docs_src/tutorial/create_db_and_table/tutorial002_py310.py @@ -0,0 +1,22 @@ +from sqlmodel import Field, SQLModel, create_engine + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +if __name__ == "__main__": + create_db_and_tables() diff --git a/docs_src/tutorial/create_db_and_table/tutorial002.py b/docs_src/tutorial/create_db_and_table/tutorial002_py39.py similarity index 100% rename from docs_src/tutorial/create_db_and_table/tutorial002.py rename to docs_src/tutorial/create_db_and_table/tutorial002_py39.py diff --git a/docs_src/tutorial/create_db_and_table/tutorial003.py b/docs_src/tutorial/create_db_and_table/tutorial003.py deleted file mode 100644 index 86508709c1..0000000000 --- a/docs_src/tutorial/create_db_and_table/tutorial003.py +++ /dev/null @@ -1,24 +0,0 @@ -from typing import Optional # (1) - -from sqlmodel import Field, SQLModel, create_engine # (2) - - -class Hero(SQLModel, table=True): # (3) - id: Optional[int] = Field(default=None, primary_key=True) # (4) - name: str # (5) - secret_name: str # (6) - age: Optional[int] = None # (7) - - -sqlite_file_name = "database.db" # (8) -sqlite_url = f"sqlite:///{sqlite_file_name}" # (9) - -engine = create_engine(sqlite_url, echo=True) # (10) - - -def create_db_and_tables(): # (11) - SQLModel.metadata.create_all(engine) # (12) - - -if __name__ == "__main__": # (13) - create_db_and_tables() # (14) diff --git a/docs_src/tutorial/create_db_and_table/tutorial003_py310.py b/docs_src/tutorial/create_db_and_table/tutorial003_py310.py new file mode 100644 index 0000000000..01fcb14ac2 --- /dev/null +++ b/docs_src/tutorial/create_db_and_table/tutorial003_py310.py @@ -0,0 +1,22 @@ +from sqlmodel import Field, SQLModel, create_engine # (2)! + + +class Hero(SQLModel, table=True): # (3)! + id: int | None = Field(default=None, primary_key=True) # (4)! + name: str # (5)! + secret_name: str # (6)! + age: int | None = None # (7)! + + +sqlite_file_name = "database.db" # (8)! +sqlite_url = f"sqlite:///{sqlite_file_name}" # (9)! + +engine = create_engine(sqlite_url, echo=True) # (10)! + + +def create_db_and_tables(): # (11)! + SQLModel.metadata.create_all(engine) # (12)! + + +if __name__ == "__main__": # (13)! + create_db_and_tables() # (14)! diff --git a/docs_src/tutorial/create_db_and_table/tutorial003_py39.py b/docs_src/tutorial/create_db_and_table/tutorial003_py39.py new file mode 100644 index 0000000000..9406300400 --- /dev/null +++ b/docs_src/tutorial/create_db_and_table/tutorial003_py39.py @@ -0,0 +1,24 @@ +from typing import Optional # (1)! + +from sqlmodel import Field, SQLModel, create_engine # (2)! + + +class Hero(SQLModel, table=True): # (3)! + id: Optional[int] = Field(default=None, primary_key=True) # (4)! + name: str # (5)! + secret_name: str # (6)! + age: Optional[int] = None # (7)! + + +sqlite_file_name = "database.db" # (8)! +sqlite_url = f"sqlite:///{sqlite_file_name}" # (9)! + +engine = create_engine(sqlite_url, echo=True) # (10)! + + +def create_db_and_tables(): # (11)! + SQLModel.metadata.create_all(engine) # (12)! + + +if __name__ == "__main__": # (13)! + create_db_and_tables() # (14)! diff --git a/docs_src/tutorial/delete/annotations/en/tutorial002.md b/docs_src/tutorial/delete/annotations/en/tutorial002.md index 130016daec..a6971f67d5 100644 --- a/docs_src/tutorial/delete/annotations/en/tutorial002.md +++ b/docs_src/tutorial/delete/annotations/en/tutorial002.md @@ -6,18 +6,21 @@ ``` INFO Engine BEGIN (implicit) - INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age - FROM hero + INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age + FROM hero WHERE hero.name = ? INFO Engine [no key 0.00011s] ('Spider-Youngster',) ``` 3. Get one hero object, expecting exactly one. - !!! tip - This ensures there's no more than one, and that there's exactly one, not `None`. + /// tip - This would never return `None`, instead it would raise an exception. + This ensures there's no more than one, and that there's exactly one, not `None`. + + This would never return `None`, instead it would raise an exception. + + /// 4. Print the hero object. @@ -65,8 +68,8 @@ ``` INFO Engine BEGIN (implicit) - INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age - FROM hero + INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age + FROM hero WHERE hero.name = ? INFO Engine [no key 0.00013s] ('Spider-Youngster',) ``` diff --git a/docs_src/tutorial/delete/tutorial001_py310.py b/docs_src/tutorial/delete/tutorial001_py310.py new file mode 100644 index 0000000000..7c29efa3c3 --- /dev/null +++ b/docs_src/tutorial/delete/tutorial001_py310.py @@ -0,0 +1,98 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def update_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Spider-Boy") + results = session.exec(statement) + hero_1 = results.one() + print("Hero 1:", hero_1) + + statement = select(Hero).where(Hero.name == "Captain North America") + results = session.exec(statement) + hero_2 = results.one() + print("Hero 2:", hero_2) + + hero_1.age = 16 + hero_1.name = "Spider-Youngster" + session.add(hero_1) + + hero_2.name = "Captain North America Except Canada" + hero_2.age = 110 + session.add(hero_2) + + session.commit() + session.refresh(hero_1) + session.refresh(hero_2) + + print("Updated hero 1:", hero_1) + print("Updated hero 2:", hero_2) + + +def delete_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Spider-Youngster") + results = session.exec(statement) + hero = results.one() + print("Hero: ", hero) + + session.delete(hero) + session.commit() + + print("Deleted hero:", hero) + + statement = select(Hero).where(Hero.name == "Spider-Youngster") + results = session.exec(statement) + hero = results.first() + + if hero is None: + print("There's no hero named Spider-Youngster") + + +def main(): + create_db_and_tables() + create_heroes() + update_heroes() + delete_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/delete/tutorial001.py b/docs_src/tutorial/delete/tutorial001_py39.py similarity index 100% rename from docs_src/tutorial/delete/tutorial001.py rename to docs_src/tutorial/delete/tutorial001_py39.py diff --git a/docs_src/tutorial/delete/tutorial002_py310.py b/docs_src/tutorial/delete/tutorial002_py310.py new file mode 100644 index 0000000000..afe9a4764d --- /dev/null +++ b/docs_src/tutorial/delete/tutorial002_py310.py @@ -0,0 +1,99 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def update_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Spider-Boy") + results = session.exec(statement) + hero_1 = results.one() + print("Hero 1:", hero_1) + + statement = select(Hero).where(Hero.name == "Captain North America") + results = session.exec(statement) + hero_2 = results.one() + print("Hero 2:", hero_2) + + hero_1.age = 16 + hero_1.name = "Spider-Youngster" + session.add(hero_1) + + hero_2.name = "Captain North America Except Canada" + hero_2.age = 110 + session.add(hero_2) + + session.commit() + session.refresh(hero_1) + session.refresh(hero_2) + + print("Updated hero 1:", hero_1) + print("Updated hero 2:", hero_2) + + +def delete_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Spider-Youngster") # (1)! + results = session.exec(statement) # (2)! + hero = results.one() # (3)! + print("Hero: ", hero) # (4)! + + session.delete(hero) # (5)! + session.commit() # (6)! + + print("Deleted hero:", hero) # (7)! + + statement = select(Hero).where(Hero.name == "Spider-Youngster") # (8)! + results = session.exec(statement) # (9)! + hero = results.first() # (10)! + + if hero is None: # (11)! + print("There's no hero named Spider-Youngster") # (12)! + # (13)! + + +def main(): + create_db_and_tables() + create_heroes() + update_heroes() + delete_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/delete/tutorial002.py b/docs_src/tutorial/delete/tutorial002_py39.py similarity index 85% rename from docs_src/tutorial/delete/tutorial002.py rename to docs_src/tutorial/delete/tutorial002_py39.py index 202d63b6d3..4d8c368d3c 100644 --- a/docs_src/tutorial/delete/tutorial002.py +++ b/docs_src/tutorial/delete/tutorial002_py39.py @@ -71,23 +71,23 @@ def update_heroes(): def delete_heroes(): with Session(engine) as session: - statement = select(Hero).where(Hero.name == "Spider-Youngster") # (1) - results = session.exec(statement) # (2) - hero = results.one() # (3) - print("Hero: ", hero) # (4) + statement = select(Hero).where(Hero.name == "Spider-Youngster") # (1)! + results = session.exec(statement) # (2)! + hero = results.one() # (3)! + print("Hero: ", hero) # (4)! - session.delete(hero) # (5) - session.commit() # (6) + session.delete(hero) # (5)! + session.commit() # (6)! - print("Deleted hero:", hero) # (7) + print("Deleted hero:", hero) # (7)! - statement = select(Hero).where(Hero.name == "Spider-Youngster") # (8) - results = session.exec(statement) # (9) - hero = results.first() # (10) + statement = select(Hero).where(Hero.name == "Spider-Youngster") # (8)! + results = session.exec(statement) # (9)! + hero = results.first() # (10)! - if hero is None: # (11) - print("There's no hero named Spider-Youngster") # (12) - # (13) + if hero is None: # (11)! + print("There's no hero named Spider-Youngster") # (12)! + # (13)! def main(): diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/__init__.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_001.md b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_001.md similarity index 100% rename from docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_001.md rename to docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_001.md diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_002.md b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_002.md similarity index 100% rename from docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_002.md rename to docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_002.md diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_003.md b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_003.md similarity index 100% rename from docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_003.md rename to docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_003.md diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_004.md b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_004.md similarity index 73% rename from docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_004.md rename to docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_004.md index 92cbe77441..de754c5e76 100644 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_004.md +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_004.md @@ -22,5 +22,8 @@ We tell it that with the `poolclass=StaticPool` parameter. - !!! info - You can read more details in the SQLAlchemy documentation about Using a Memory Database in Multiple Threads + /// info + + You can read more details in the SQLAlchemy documentation about Using a Memory Database in Multiple Threads + + /// diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_005.md b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_005.md similarity index 100% rename from docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_005.md rename to docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_005.md diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_006.md b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_006.md similarity index 100% rename from docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_006.md rename to docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_006.md diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/main.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/main.py new file mode 100644 index 0000000000..84da9fd610 --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/main.py @@ -0,0 +1,103 @@ +from fastapi import Depends, FastAPI, HTTPException, Query +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class HeroBase(SQLModel): + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +class Hero(HeroBase, table=True): + id: int | None = Field(default=None, primary_key=True) + + +class HeroCreate(HeroBase): + pass + + +class HeroPublic(HeroBase): + id: int + + +class HeroUpdate(SQLModel): + name: str | None = None + secret_name: str | None = None + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +connect_args = {"check_same_thread": False} +engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def get_session(): + with Session(engine) as session: + yield session + + +app = FastAPI() + + +@app.on_event("startup") +def on_startup(): + create_db_and_tables() + + +@app.post("/heroes/", response_model=HeroPublic) +def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): + db_hero = Hero.model_validate(hero) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.get("/heroes/", response_model=list[HeroPublic]) +def read_heroes( + *, + session: Session = Depends(get_session), + offset: int = 0, + limit: int = Query(default=100, le=100), +): + heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() + return heroes + + +@app.get("/heroes/{hero_id}", response_model=HeroPublic) +def read_hero(*, session: Session = Depends(get_session), hero_id: int): + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + return hero + + +@app.patch("/heroes/{hero_id}", response_model=HeroPublic) +def update_hero( + *, session: Session = Depends(get_session), hero_id: int, hero: HeroUpdate +): + db_hero = session.get(Hero, hero_id) + if not db_hero: + raise HTTPException(status_code=404, detail="Hero not found") + hero_data = hero.model_dump(exclude_unset=True) + db_hero.sqlmodel_update(hero_data) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.delete("/heroes/{hero_id}") +def delete_hero(*, session: Session = Depends(get_session), hero_id: int): + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + session.delete(hero) + session.commit() + return {"ok": True} diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_extra_coverage.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_extra_coverage.py new file mode 100644 index 0000000000..1d8153ab9f --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_extra_coverage.py @@ -0,0 +1,38 @@ +from fastapi.testclient import TestClient +from sqlalchemy import Inspector, inspect +from sqlmodel import Session, create_engine + +from . import main as app_mod +from .test_main import client_fixture, session_fixture + +assert client_fixture, "This keeps the client fixture used below" +assert session_fixture, "This keeps the session fixture used by client_fixture" + + +def test_startup(): + app_mod.engine = create_engine("sqlite://") + app_mod.on_startup() + insp: Inspector = inspect(app_mod.engine) + assert insp.has_table(str(app_mod.Hero.__tablename__)) + + +def test_get_session(): + app_mod.engine = create_engine("sqlite://") + for session in app_mod.get_session(): + assert isinstance(session, Session) + assert session.bind == app_mod.engine + + +def test_read_hero_not_found(client: TestClient): + response = client.get("/heroes/9000") + assert response.status_code == 404 + + +def test_update_hero_not_found(client: TestClient): + response = client.patch("/heroes/9000", json={"name": "Very-Rusty-Man"}) + assert response.status_code == 404 + + +def test_delete_hero_not_found(client: TestClient): + response = client.delete("/heroes/9000") + assert response.status_code == 404 diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main.py similarity index 100% rename from docs_src/tutorial/fastapi/app_testing/tutorial001/test_main.py rename to docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main.py diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_001.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_001.py similarity index 58% rename from docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_001.py rename to docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_001.py index 38ab1f1885..3ae40773f9 100644 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_001.py +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_001.py @@ -1,7 +1,7 @@ from fastapi.testclient import TestClient from sqlmodel import Session, SQLModel, create_engine -from .main import app, get_session # (1) +from .main import app, get_session # (1)! def test_create_hero(): @@ -17,16 +17,16 @@ def get_session_override(): app.dependency_overrides[get_session] = get_session_override - client = TestClient(app) # (2) + client = TestClient(app) # (2)! - response = client.post( # (3) + response = client.post( # (3)! "/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"} ) app.dependency_overrides.clear() - data = response.json() # (4) + data = response.json() # (4)! - assert response.status_code == 200 # (5) - assert data["name"] == "Deadpond" # (6) - assert data["secret_name"] == "Dive Wilson" # (7) - assert data["age"] is None # (8) - assert data["id"] is not None # (9) + assert response.status_code == 200 # (5)! + assert data["name"] == "Deadpond" # (6)! + assert data["secret_name"] == "Dive Wilson" # (7)! + assert data["age"] is None # (8)! + assert data["id"] is not None # (9)! diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_002.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_002.py similarity index 80% rename from docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_002.py rename to docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_002.py index 144360a820..727580b68f 100644 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_002.py +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_002.py @@ -1,7 +1,7 @@ from fastapi.testclient import TestClient from sqlmodel import Session, SQLModel, create_engine -from .main import app, get_session # (1) +from .main import app, get_session # (1)! def test_create_hero(): @@ -12,17 +12,17 @@ def test_create_hero(): with Session(engine) as session: - def get_session_override(): # (2) - return session # (3) + def get_session_override(): # (2)! + return session # (3)! - app.dependency_overrides[get_session] = get_session_override # (4) + app.dependency_overrides[get_session] = get_session_override # (4)! client = TestClient(app) response = client.post( "/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"} ) - app.dependency_overrides.clear() # (5) + app.dependency_overrides.clear() # (5)! data = response.json() assert response.status_code == 200 diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_003.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_003.py similarity index 76% rename from docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_003.py rename to docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_003.py index 5de06f8404..465c525108 100644 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_003.py +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_003.py @@ -1,21 +1,21 @@ from fastapi.testclient import TestClient from sqlmodel import Session, SQLModel, create_engine -from .main import app, get_session # (1) +from .main import app, get_session # (1)! def test_create_hero(): - engine = create_engine( # (2) + engine = create_engine( # (2)! "sqlite:///testing.db", connect_args={"check_same_thread": False} ) - SQLModel.metadata.create_all(engine) # (3) + SQLModel.metadata.create_all(engine) # (3)! - with Session(engine) as session: # (4) + with Session(engine) as session: # (4)! def get_session_override(): - return session # (5) + return session # (5)! - app.dependency_overrides[get_session] = get_session_override # (4) + app.dependency_overrides[get_session] = get_session_override # (4)! client = TestClient(app) @@ -30,4 +30,4 @@ def get_session_override(): assert data["secret_name"] == "Dive Wilson" assert data["age"] is None assert data["id"] is not None - # (6) + # (6)! diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_004.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_004.py similarity index 88% rename from docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_004.py rename to docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_004.py index f14fce8092..b770a9aa59 100644 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_004.py +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_004.py @@ -1,15 +1,15 @@ from fastapi.testclient import TestClient from sqlmodel import Session, SQLModel, create_engine -from sqlmodel.pool import StaticPool # (1) +from sqlmodel.pool import StaticPool # (1)! from .main import app, get_session def test_create_hero(): engine = create_engine( - "sqlite://", # (2) + "sqlite://", # (2)! connect_args={"check_same_thread": False}, - poolclass=StaticPool, # (3) + poolclass=StaticPool, # (3)! ) SQLModel.metadata.create_all(engine) diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_005.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_005.py similarity index 81% rename from docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_005.py rename to docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_005.py index b5200e6150..f653eef7ec 100644 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_005.py +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_005.py @@ -1,4 +1,4 @@ -import pytest # (1) +import pytest # (1)! from fastapi.testclient import TestClient from sqlmodel import Session, SQLModel, create_engine from sqlmodel.pool import StaticPool @@ -6,19 +6,19 @@ from .main import app, get_session -@pytest.fixture(name="session") # (2) -def session_fixture(): # (3) +@pytest.fixture(name="session") # (2)! +def session_fixture(): # (3)! engine = create_engine( "sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool ) SQLModel.metadata.create_all(engine) with Session(engine) as session: - yield session # (4) + yield session # (4)! -def test_create_hero(session: Session): # (5) +def test_create_hero(session: Session): # (5)! def get_session_override(): - return session # (6) + return session # (6)! app.dependency_overrides[get_session] = get_session_override diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_006.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_006.py similarity index 75% rename from docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_006.py rename to docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_006.py index 577d56ddaf..8dbfd45caf 100644 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_006.py +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_006.py @@ -16,19 +16,19 @@ def session_fixture(): yield session -@pytest.fixture(name="client") # (1) -def client_fixture(session: Session): # (2) - def get_session_override(): # (3) +@pytest.fixture(name="client") # (1)! +def client_fixture(session: Session): # (2)! + def get_session_override(): # (3)! return session - app.dependency_overrides[get_session] = get_session_override # (4) + app.dependency_overrides[get_session] = get_session_override # (4)! - client = TestClient(app) # (5) - yield client # (6) - app.dependency_overrides.clear() # (7) + client = TestClient(app) # (5)! + yield client # (6)! + app.dependency_overrides.clear() # (7)! -def test_create_hero(client: TestClient): # (8) +def test_create_hero(client: TestClient): # (8)! response = client.post( "/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"} ) diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/__init__.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_001.md b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_001.md new file mode 100644 index 0000000000..936b84b92d --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_001.md @@ -0,0 +1,17 @@ +1. Import the `app` from the the `main` module. + +2. We create a `TestClient` for the FastAPI `app` and put it in the variable `client`. + +3. Then we use use this `client` to **talk to the API** and send a `POST` HTTP operation, creating a new hero. + +4. Then we get the **JSON data** from the response and put it in the variable `data`. + +5. Next we start testing the results with `assert` statements, we check that the status code of the response is `200`. + +6. We check that the `name` of the hero created is `"Deadpond"`. + +7. We check that the `secret_name` of the hero created is `"Dive Wilson"`. + +8. We check that the `age` of the hero created is `None`, because we didn't send an age. + +9. We check that the hero created has an `id` created by the database, so it's not `None`. diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_002.md b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_002.md new file mode 100644 index 0000000000..0f8555a8dd --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_002.md @@ -0,0 +1,25 @@ +1. Import the `get_session` dependency from the the `main` module. + +2. Define the new function that will be the new **dependency override**. + +3. This function will return a different **session** than the one that would be returned by the original `get_session` function. + + We haven't seen how this new **session** object is created yet, but the point is that this is a different session than the original one from the app. + + This session is attached to a different **engine**, and that different **engine** uses a different URL, for a database just for testing. + + We haven't defined that new **URL** nor the new **engine** yet, but here we already see the that this object `session` will override the one returned by the original dependency `get_session()`. + +4. Then, the FastAPI `app` object has an attribute `app.dependency_overrides`. + + This attribute is a dictionary, and we can put dependency overrides in it by passing, as the **key**, the **original dependency function**, and as the **value**, the **new overriding dependency function**. + + So, here we are telling the FastAPI app to use `get_session_override` instead of `get_session` in all the places in the code that depend on `get_session`, that is, all the parameters with something like: + + ```Python + session: Session = Depends(get_session) + ``` + +5. After we are done with the dependency override, we can restore the application back to normal, by removing all the values in this dictionary `app.dependency_overrides`. + + This way whenever a *path operation function* needs the dependency FastAPI will use the original one instead of the override. diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_003.md b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_003.md new file mode 100644 index 0000000000..2b48ebdacf --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_003.md @@ -0,0 +1,37 @@ +1. Here's a subtle thing to notice. + + Remember that [Order Matters](../create-db-and-table.md#sqlmodel-metadata-order-matters){.internal-link target=_blank} and we need to make sure all the **SQLModel** models are already defined and **imported** before calling `.create_all()`. + + IN this line, by importing something, *anything*, from `.main`, the code in `.main` will be executed, including the definition of the **table models**, and that will automatically register them in `SQLModel.metadata`. + +2. Here we create a new **engine**, completely different from the one in `main.py`. + + This is the engine we will use for the tests. + + We use the new URL of the database for tests: + + ``` + sqlite:///testing.db + ``` + + And again, we use the connection argument `check_same_thread=False`. + +3. Then we call: + + ```Python + SQLModel.metadata.create_all(engine) + ``` + + ...to make sure we create all the tables in the new testing database. + + The **table models** are registered in `SQLModel.metadata` just because we imported *something* from `.main`, and the code in `.main` was executed, creating the classes for the **table models** and automatically registering them in `SQLModel.metadata`. + + So, by the point we call this method, the **table models** are already registered there. 💯 + +4. Here's where we create the custom **session** object for this test in a `with` block. + + It uses the new custom **engine** we created, so anything that uses this session will be using the testing database. + +5. Now, back to the dependency override, it is just returning the same **session** object from outside, that's it, that's the whole trick. + +6. By this point, the testing **session** `with` block finishes, and the session is closed, the file is closed, etc. diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_004.md b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_004.md new file mode 100644 index 0000000000..de754c5e76 --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_004.md @@ -0,0 +1,29 @@ +1. Import `StaticPool` from `sqlmodel`, we will use it in a bit. + +2. For the **SQLite URL**, don't write any file name, leave it empty. + + So, instead of: + + ``` + sqlite:///testing.db + ``` + + ...just write: + + ``` + sqlite:// + ``` + + This is enough to tell **SQLModel** (actually SQLAlchemy) that we want to use an **in-memory SQLite database**. + +3. Remember that we told the **low-level** library in charge of communicating with SQLite that we want to be able to **access the database from different threads** with `check_same_thread=False`? + + Now that we use an **in-memory database**, we need to also tell SQLAlchemy that we want to be able to use the **same in-memory database** object from different threads. + + We tell it that with the `poolclass=StaticPool` parameter. + + /// info + + You can read more details in the SQLAlchemy documentation about Using a Memory Database in Multiple Threads + + /// diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_005.md b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_005.md new file mode 100644 index 0000000000..126e1f1790 --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_005.md @@ -0,0 +1,41 @@ +1. Import `pytest`. + +2. Use the `@pytest.fixture()` decorator on top of the function to tell pytest that this is a **fixture** function (equivalent to a FastAPI dependency). + + We also give it a name of `"session"`, this will be important in the testing function. + +3. Create the fixture function. This is equivalent to a FastAPI dependency function. + + In this fixture we create the custom **engine**, with the in-memory database, we create the tables, and we create the **session**. + + Then we `yield` the `session` object. + +4. The thing that we `return` or `yield` is what will be available to the test function, in this case, the `session` object. + + Here we use `yield` so that **pytest** comes back to execute "the rest of the code" in this function once the testing function is done. + + We don't have any more visible "rest of the code" after the `yield`, but we have the end of the `with` block that will close the **session**. + + By using `yield`, pytest will: + + * run the first part + * create the **session** object + * give it to the test function + * run the test function + * once the test function is done, it will continue here, right after the `yield`, and will correctly close the **session** object in the end of the `with` block. + +5. Now, in the test function, to tell **pytest** that this test wants to get the fixture, instead of declaring something like in FastAPI with: + + ```Python + session: Session = Depends(session_fixture) + ``` + + ...the way we tell pytest what is the fixture that we want is by using the **exact same name** of the fixture. + + In this case, we named it `session`, so the parameter has to be exactly named `session` for it to work. + + We also add the type annotation `session: Session` so that we can get autocompletion and inline error checks in our editor. + +6. Now in the dependency override function, we just return the same `session` object that came from outside it. + + The `session` object comes from the parameter passed to the test function, and we just re-use it and return it here in the dependency override. diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_006.md b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_006.md new file mode 100644 index 0000000000..d44a3b67da --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_006.md @@ -0,0 +1,23 @@ +1. Create the new fixture named `"client"`. + +2. This **client fixture**, in turn, also requires the **session fixture**. + +3. Now we create the **dependency override** inside the client fixture. + +4. Set the **dependency override** in the `app.dependency_overrides` dictionary. + +5. Create the `TestClient` with the **FastAPI** `app`. + +6. `yield` the `TestClient` instance. + + By using `yield`, after the test function is done, pytest will come back to execute the rest of the code after `yield`. + +7. This is the cleanup code, after `yield`, and after the test function is done. + + Here we clear the dependency overrides (here it's only one) in the FastAPI `app`. + +8. Now the test function requires the **client fixture**. + + And inside the test function, the code is quite **simple**, we just use the `TestClient` to make requests to the API, check the data, and that's it. + + The fixtures take care of all the **setup** and **cleanup** code. diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/main.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/main.py similarity index 82% rename from docs_src/tutorial/fastapi/app_testing/tutorial001/main.py rename to docs_src/tutorial/fastapi/app_testing/tutorial001_py39/main.py index 88b8fbbcea..e7371d84e3 100644 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001/main.py +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/main.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Optional from fastapi import Depends, FastAPI, HTTPException, Query from sqlmodel import Field, Session, SQLModel, create_engine, select @@ -18,7 +18,7 @@ class HeroCreate(HeroBase): pass -class HeroRead(HeroBase): +class HeroPublic(HeroBase): id: int @@ -52,27 +52,27 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): - db_hero = Hero.from_orm(hero) + db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) return db_hero -@app.get("/heroes/", response_model=List[HeroRead]) +@app.get("/heroes/", response_model=list[HeroPublic]) def read_heroes( *, session: Session = Depends(get_session), offset: int = 0, - limit: int = Query(default=100, lte=100), + limit: int = Query(default=100, le=100), ): heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() return heroes -@app.get("/heroes/{hero_id}", response_model=HeroRead) +@app.get("/heroes/{hero_id}", response_model=HeroPublic) def read_hero(*, session: Session = Depends(get_session), hero_id: int): hero = session.get(Hero, hero_id) if not hero: @@ -80,16 +80,15 @@ def read_hero(*, session: Session = Depends(get_session), hero_id: int): return hero -@app.patch("/heroes/{hero_id}", response_model=HeroRead) +@app.patch("/heroes/{hero_id}", response_model=HeroPublic) def update_hero( *, session: Session = Depends(get_session), hero_id: int, hero: HeroUpdate ): db_hero = session.get(Hero, hero_id) if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") - hero_data = hero.dict(exclude_unset=True) - for key, value in hero_data.items(): - setattr(db_hero, key, value) + hero_data = hero.model_dump(exclude_unset=True) + db_hero.sqlmodel_update(hero_data) session.add(db_hero) session.commit() session.refresh(db_hero) @@ -98,7 +97,6 @@ def update_hero( @app.delete("/heroes/{hero_id}") def delete_hero(*, session: Session = Depends(get_session), hero_id: int): - hero = session.get(Hero, hero_id) if not hero: raise HTTPException(status_code=404, detail="Hero not found") diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_extra_coverage.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_extra_coverage.py new file mode 100644 index 0000000000..1d8153ab9f --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_extra_coverage.py @@ -0,0 +1,38 @@ +from fastapi.testclient import TestClient +from sqlalchemy import Inspector, inspect +from sqlmodel import Session, create_engine + +from . import main as app_mod +from .test_main import client_fixture, session_fixture + +assert client_fixture, "This keeps the client fixture used below" +assert session_fixture, "This keeps the session fixture used by client_fixture" + + +def test_startup(): + app_mod.engine = create_engine("sqlite://") + app_mod.on_startup() + insp: Inspector = inspect(app_mod.engine) + assert insp.has_table(str(app_mod.Hero.__tablename__)) + + +def test_get_session(): + app_mod.engine = create_engine("sqlite://") + for session in app_mod.get_session(): + assert isinstance(session, Session) + assert session.bind == app_mod.engine + + +def test_read_hero_not_found(client: TestClient): + response = client.get("/heroes/9000") + assert response.status_code == 404 + + +def test_update_hero_not_found(client: TestClient): + response = client.patch("/heroes/9000", json={"name": "Very-Rusty-Man"}) + assert response.status_code == 404 + + +def test_delete_hero_not_found(client: TestClient): + response = client.delete("/heroes/9000") + assert response.status_code == 404 diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main.py new file mode 100644 index 0000000000..435787c79b --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main.py @@ -0,0 +1,125 @@ +import pytest +from fastapi.testclient import TestClient +from sqlmodel import Session, SQLModel, create_engine +from sqlmodel.pool import StaticPool + +from .main import Hero, app, get_session + + +@pytest.fixture(name="session") +def session_fixture(): + engine = create_engine( + "sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool + ) + SQLModel.metadata.create_all(engine) + with Session(engine) as session: + yield session + + +@pytest.fixture(name="client") +def client_fixture(session: Session): + def get_session_override(): + return session + + app.dependency_overrides[get_session] = get_session_override + client = TestClient(app) + yield client + app.dependency_overrides.clear() + + +def test_create_hero(client: TestClient): + response = client.post( + "/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"} + ) + data = response.json() + + assert response.status_code == 200 + assert data["name"] == "Deadpond" + assert data["secret_name"] == "Dive Wilson" + assert data["age"] is None + assert data["id"] is not None + + +def test_create_hero_incomplete(client: TestClient): + # No secret_name + response = client.post("/heroes/", json={"name": "Deadpond"}) + assert response.status_code == 422 + + +def test_create_hero_invalid(client: TestClient): + # secret_name has an invalid type + response = client.post( + "/heroes/", + json={ + "name": "Deadpond", + "secret_name": {"message": "Do you wanna know my secret identity?"}, + }, + ) + assert response.status_code == 422 + + +def test_read_heroes(session: Session, client: TestClient): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + session.add(hero_1) + session.add(hero_2) + session.commit() + + response = client.get("/heroes/") + data = response.json() + + assert response.status_code == 200 + + assert len(data) == 2 + assert data[0]["name"] == hero_1.name + assert data[0]["secret_name"] == hero_1.secret_name + assert data[0]["age"] == hero_1.age + assert data[0]["id"] == hero_1.id + assert data[1]["name"] == hero_2.name + assert data[1]["secret_name"] == hero_2.secret_name + assert data[1]["age"] == hero_2.age + assert data[1]["id"] == hero_2.id + + +def test_read_hero(session: Session, client: TestClient): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + session.add(hero_1) + session.commit() + + response = client.get(f"/heroes/{hero_1.id}") + data = response.json() + + assert response.status_code == 200 + assert data["name"] == hero_1.name + assert data["secret_name"] == hero_1.secret_name + assert data["age"] == hero_1.age + assert data["id"] == hero_1.id + + +def test_update_hero(session: Session, client: TestClient): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + session.add(hero_1) + session.commit() + + response = client.patch(f"/heroes/{hero_1.id}", json={"name": "Deadpuddle"}) + data = response.json() + + assert response.status_code == 200 + assert data["name"] == "Deadpuddle" + assert data["secret_name"] == "Dive Wilson" + assert data["age"] is None + assert data["id"] == hero_1.id + + +def test_delete_hero(session: Session, client: TestClient): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + session.add(hero_1) + session.commit() + + response = client.delete(f"/heroes/{hero_1.id}") + + hero_in_db = session.get(Hero, hero_1.id) + + assert response.status_code == 200 + + assert hero_in_db is None diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_001.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_001.py new file mode 100644 index 0000000000..3ae40773f9 --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_001.py @@ -0,0 +1,32 @@ +from fastapi.testclient import TestClient +from sqlmodel import Session, SQLModel, create_engine + +from .main import app, get_session # (1)! + + +def test_create_hero(): + engine = create_engine( + "sqlite:///testing.db", connect_args={"check_same_thread": False} + ) + SQLModel.metadata.create_all(engine) + + with Session(engine) as session: + + def get_session_override(): + return session + + app.dependency_overrides[get_session] = get_session_override + + client = TestClient(app) # (2)! + + response = client.post( # (3)! + "/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"} + ) + app.dependency_overrides.clear() + data = response.json() # (4)! + + assert response.status_code == 200 # (5)! + assert data["name"] == "Deadpond" # (6)! + assert data["secret_name"] == "Dive Wilson" # (7)! + assert data["age"] is None # (8)! + assert data["id"] is not None # (9)! diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_002.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_002.py new file mode 100644 index 0000000000..727580b68f --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_002.py @@ -0,0 +1,32 @@ +from fastapi.testclient import TestClient +from sqlmodel import Session, SQLModel, create_engine + +from .main import app, get_session # (1)! + + +def test_create_hero(): + engine = create_engine( + "sqlite:///testing.db", connect_args={"check_same_thread": False} + ) + SQLModel.metadata.create_all(engine) + + with Session(engine) as session: + + def get_session_override(): # (2)! + return session # (3)! + + app.dependency_overrides[get_session] = get_session_override # (4)! + + client = TestClient(app) + + response = client.post( + "/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"} + ) + app.dependency_overrides.clear() # (5)! + data = response.json() + + assert response.status_code == 200 + assert data["name"] == "Deadpond" + assert data["secret_name"] == "Dive Wilson" + assert data["age"] is None + assert data["id"] is not None diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_003.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_003.py new file mode 100644 index 0000000000..465c525108 --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_003.py @@ -0,0 +1,33 @@ +from fastapi.testclient import TestClient +from sqlmodel import Session, SQLModel, create_engine + +from .main import app, get_session # (1)! + + +def test_create_hero(): + engine = create_engine( # (2)! + "sqlite:///testing.db", connect_args={"check_same_thread": False} + ) + SQLModel.metadata.create_all(engine) # (3)! + + with Session(engine) as session: # (4)! + + def get_session_override(): + return session # (5)! + + app.dependency_overrides[get_session] = get_session_override # (4)! + + client = TestClient(app) + + response = client.post( + "/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"} + ) + app.dependency_overrides.clear() + data = response.json() + + assert response.status_code == 200 + assert data["name"] == "Deadpond" + assert data["secret_name"] == "Dive Wilson" + assert data["age"] is None + assert data["id"] is not None + # (6)! diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_004.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_004.py new file mode 100644 index 0000000000..b770a9aa59 --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_004.py @@ -0,0 +1,35 @@ +from fastapi.testclient import TestClient +from sqlmodel import Session, SQLModel, create_engine +from sqlmodel.pool import StaticPool # (1)! + +from .main import app, get_session + + +def test_create_hero(): + engine = create_engine( + "sqlite://", # (2)! + connect_args={"check_same_thread": False}, + poolclass=StaticPool, # (3)! + ) + SQLModel.metadata.create_all(engine) + + with Session(engine) as session: + + def get_session_override(): + return session + + app.dependency_overrides[get_session] = get_session_override + + client = TestClient(app) + + response = client.post( + "/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"} + ) + app.dependency_overrides.clear() + data = response.json() + + assert response.status_code == 200 + assert data["name"] == "Deadpond" + assert data["secret_name"] == "Dive Wilson" + assert data["age"] is None + assert data["id"] is not None diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_005.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_005.py new file mode 100644 index 0000000000..f653eef7ec --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_005.py @@ -0,0 +1,37 @@ +import pytest # (1)! +from fastapi.testclient import TestClient +from sqlmodel import Session, SQLModel, create_engine +from sqlmodel.pool import StaticPool + +from .main import app, get_session + + +@pytest.fixture(name="session") # (2)! +def session_fixture(): # (3)! + engine = create_engine( + "sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool + ) + SQLModel.metadata.create_all(engine) + with Session(engine) as session: + yield session # (4)! + + +def test_create_hero(session: Session): # (5)! + def get_session_override(): + return session # (6)! + + app.dependency_overrides[get_session] = get_session_override + + client = TestClient(app) + + response = client.post( + "/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"} + ) + app.dependency_overrides.clear() + data = response.json() + + assert response.status_code == 200 + assert data["name"] == "Deadpond" + assert data["secret_name"] == "Dive Wilson" + assert data["age"] is None + assert data["id"] is not None diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_006.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_006.py new file mode 100644 index 0000000000..8dbfd45caf --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_006.py @@ -0,0 +1,41 @@ +import pytest +from fastapi.testclient import TestClient +from sqlmodel import Session, SQLModel, create_engine +from sqlmodel.pool import StaticPool + +from .main import app, get_session + + +@pytest.fixture(name="session") +def session_fixture(): + engine = create_engine( + "sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool + ) + SQLModel.metadata.create_all(engine) + with Session(engine) as session: + yield session + + +@pytest.fixture(name="client") # (1)! +def client_fixture(session: Session): # (2)! + def get_session_override(): # (3)! + return session + + app.dependency_overrides[get_session] = get_session_override # (4)! + + client = TestClient(app) # (5)! + yield client # (6)! + app.dependency_overrides.clear() # (7)! + + +def test_create_hero(client: TestClient): # (8)! + response = client.post( + "/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"} + ) + data = response.json() + + assert response.status_code == 200 + assert data["name"] == "Deadpond" + assert data["secret_name"] == "Dive Wilson" + assert data["age"] is None + assert data["id"] is not None diff --git a/docs_src/tutorial/fastapi/delete/tutorial001_py310.py b/docs_src/tutorial/fastapi/delete/tutorial001_py310.py new file mode 100644 index 0000000000..f7de4019d2 --- /dev/null +++ b/docs_src/tutorial/fastapi/delete/tutorial001_py310.py @@ -0,0 +1,96 @@ +from fastapi import FastAPI, HTTPException, Query +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class HeroBase(SQLModel): + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +class Hero(HeroBase, table=True): + id: int | None = Field(default=None, primary_key=True) + + +class HeroCreate(HeroBase): + pass + + +class HeroPublic(HeroBase): + id: int + + +class HeroUpdate(SQLModel): + name: str | None = None + secret_name: str | None = None + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +connect_args = {"check_same_thread": False} +engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +app = FastAPI() + + +@app.on_event("startup") +def on_startup(): + create_db_and_tables() + + +@app.post("/heroes/", response_model=HeroPublic) +def create_hero(hero: HeroCreate): + with Session(engine) as session: + db_hero = Hero.model_validate(hero) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.get("/heroes/", response_model=list[HeroPublic]) +def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)): + with Session(engine) as session: + heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() + return heroes + + +@app.get("/heroes/{hero_id}", response_model=HeroPublic) +def read_hero(hero_id: int): + with Session(engine) as session: + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + return hero + + +@app.patch("/heroes/{hero_id}", response_model=HeroPublic) +def update_hero(hero_id: int, hero: HeroUpdate): + with Session(engine) as session: + db_hero = session.get(Hero, hero_id) + if not db_hero: + raise HTTPException(status_code=404, detail="Hero not found") + hero_data = hero.model_dump(exclude_unset=True) + db_hero.sqlmodel_update(hero_data) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.delete("/heroes/{hero_id}") +def delete_hero(hero_id: int): + with Session(engine) as session: + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + session.delete(hero) + session.commit() + return {"ok": True} diff --git a/docs_src/tutorial/fastapi/delete/tutorial001.py b/docs_src/tutorial/fastapi/delete/tutorial001_py39.py similarity index 82% rename from docs_src/tutorial/fastapi/delete/tutorial001.py rename to docs_src/tutorial/fastapi/delete/tutorial001_py39.py index 3c15efbb2d..5d5f099abb 100644 --- a/docs_src/tutorial/fastapi/delete/tutorial001.py +++ b/docs_src/tutorial/fastapi/delete/tutorial001_py39.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Optional from fastapi import FastAPI, HTTPException, Query from sqlmodel import Field, Session, SQLModel, create_engine, select @@ -18,7 +18,7 @@ class HeroCreate(HeroBase): pass -class HeroRead(HeroBase): +class HeroPublic(HeroBase): id: int @@ -47,24 +47,24 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(hero: HeroCreate): with Session(engine) as session: - db_hero = Hero.from_orm(hero) + db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) return db_hero -@app.get("/heroes/", response_model=List[HeroRead]) -def read_heroes(offset: int = 0, limit: int = Query(default=100, lte=100)): +@app.get("/heroes/", response_model=list[HeroPublic]) +def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)): with Session(engine) as session: heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() return heroes -@app.get("/heroes/{hero_id}", response_model=HeroRead) +@app.get("/heroes/{hero_id}", response_model=HeroPublic) def read_hero(hero_id: int): with Session(engine) as session: hero = session.get(Hero, hero_id) @@ -73,15 +73,14 @@ def read_hero(hero_id: int): return hero -@app.patch("/heroes/{hero_id}", response_model=HeroRead) +@app.patch("/heroes/{hero_id}", response_model=HeroPublic) def update_hero(hero_id: int, hero: HeroUpdate): with Session(engine) as session: db_hero = session.get(Hero, hero_id) if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") - hero_data = hero.dict(exclude_unset=True) - for key, value in hero_data.items(): - setattr(db_hero, key, value) + hero_data = hero.model_dump(exclude_unset=True) + db_hero.sqlmodel_update(hero_data) session.add(db_hero) session.commit() session.refresh(db_hero) diff --git a/docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py310.py b/docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py310.py new file mode 100644 index 0000000000..3402d4045e --- /dev/null +++ b/docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py310.py @@ -0,0 +1,65 @@ +from fastapi import FastAPI, HTTPException, Query +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class HeroBase(SQLModel): + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +class Hero(HeroBase, table=True): + id: int | None = Field(default=None, primary_key=True) + + +class HeroCreate(HeroBase): + pass + + +class HeroPublic(HeroBase): + id: int + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +connect_args = {"check_same_thread": False} +engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +app = FastAPI() + + +@app.on_event("startup") +def on_startup(): + create_db_and_tables() + + +@app.post("/heroes/", response_model=HeroPublic) +def create_hero(hero: HeroCreate): + with Session(engine) as session: + db_hero = Hero.model_validate(hero) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.get("/heroes/", response_model=list[HeroPublic]) +def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)): + with Session(engine) as session: + heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() + return heroes + + +@app.get("/heroes/{hero_id}", response_model=HeroPublic) +def read_hero(hero_id: int): + with Session(engine) as session: + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + return hero diff --git a/docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py b/docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py39.py similarity index 83% rename from docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py rename to docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py39.py index aef21332a7..3d223f3290 100644 --- a/docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py +++ b/docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py39.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Optional from fastapi import FastAPI, HTTPException, Query from sqlmodel import Field, Session, SQLModel, create_engine, select @@ -18,7 +18,7 @@ class HeroCreate(HeroBase): pass -class HeroRead(HeroBase): +class HeroPublic(HeroBase): id: int @@ -41,24 +41,24 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(hero: HeroCreate): with Session(engine) as session: - db_hero = Hero.from_orm(hero) + db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) return db_hero -@app.get("/heroes/", response_model=List[HeroRead]) -def read_heroes(offset: int = 0, limit: int = Query(default=100, lte=100)): +@app.get("/heroes/", response_model=list[HeroPublic]) +def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)): with Session(engine) as session: heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() return heroes -@app.get("/heroes/{hero_id}", response_model=HeroRead) +@app.get("/heroes/{hero_id}", response_model=HeroPublic) def read_hero(hero_id: int): with Session(engine) as session: hero = session.get(Hero, hero_id) diff --git a/docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py b/docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py new file mode 100644 index 0000000000..b8dc44d981 --- /dev/null +++ b/docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py @@ -0,0 +1,58 @@ +from fastapi import FastAPI +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +class HeroCreate(SQLModel): + name: str + secret_name: str + age: int | None = None + + +class HeroPublic(SQLModel): + id: int + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +connect_args = {"check_same_thread": False} +engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +app = FastAPI() + + +@app.on_event("startup") +def on_startup(): + create_db_and_tables() + + +@app.post("/heroes/", response_model=HeroPublic) +def create_hero(hero: HeroCreate): + with Session(engine) as session: + db_hero = Hero.model_validate(hero) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.get("/heroes/", response_model=list[HeroPublic]) +def read_heroes(): + with Session(engine) as session: + heroes = session.exec(select(Hero)).all() + return heroes diff --git a/docs_src/tutorial/fastapi/multiple_models/tutorial001.py b/docs_src/tutorial/fastapi/multiple_models/tutorial001_py39.py similarity index 85% rename from docs_src/tutorial/fastapi/multiple_models/tutorial001.py rename to docs_src/tutorial/fastapi/multiple_models/tutorial001_py39.py index df20123333..c6be23d441 100644 --- a/docs_src/tutorial/fastapi/multiple_models/tutorial001.py +++ b/docs_src/tutorial/fastapi/multiple_models/tutorial001_py39.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Optional from fastapi import FastAPI from sqlmodel import Field, Session, SQLModel, create_engine, select @@ -17,7 +17,7 @@ class HeroCreate(SQLModel): age: Optional[int] = None -class HeroRead(SQLModel): +class HeroPublic(SQLModel): id: int name: str secret_name: str @@ -43,17 +43,17 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(hero: HeroCreate): with Session(engine) as session: - db_hero = Hero.from_orm(hero) + db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) return db_hero -@app.get("/heroes/", response_model=List[HeroRead]) +@app.get("/heroes/", response_model=list[HeroPublic]) def read_heroes(): with Session(engine) as session: heroes = session.exec(select(Hero)).all() diff --git a/docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py b/docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py new file mode 100644 index 0000000000..79e7447b53 --- /dev/null +++ b/docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py @@ -0,0 +1,56 @@ +from fastapi import FastAPI +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class HeroBase(SQLModel): + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +class Hero(HeroBase, table=True): + id: int | None = Field(default=None, primary_key=True) + + +class HeroCreate(HeroBase): + pass + + +class HeroPublic(HeroBase): + id: int + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +connect_args = {"check_same_thread": False} +engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +app = FastAPI() + + +@app.on_event("startup") +def on_startup(): + create_db_and_tables() + + +@app.post("/heroes/", response_model=HeroPublic) +def create_hero(hero: HeroCreate): + with Session(engine) as session: + db_hero = Hero.model_validate(hero) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.get("/heroes/", response_model=list[HeroPublic]) +def read_heroes(): + with Session(engine) as session: + heroes = session.exec(select(Hero)).all() + return heroes diff --git a/docs_src/tutorial/fastapi/multiple_models/tutorial002.py b/docs_src/tutorial/fastapi/multiple_models/tutorial002_py39.py similarity index 83% rename from docs_src/tutorial/fastapi/multiple_models/tutorial002.py rename to docs_src/tutorial/fastapi/multiple_models/tutorial002_py39.py index 392c2c5829..77093bcc7c 100644 --- a/docs_src/tutorial/fastapi/multiple_models/tutorial002.py +++ b/docs_src/tutorial/fastapi/multiple_models/tutorial002_py39.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Optional from fastapi import FastAPI from sqlmodel import Field, Session, SQLModel, create_engine, select @@ -18,7 +18,7 @@ class HeroCreate(HeroBase): pass -class HeroRead(HeroBase): +class HeroPublic(HeroBase): id: int @@ -41,17 +41,17 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(hero: HeroCreate): with Session(engine) as session: - db_hero = Hero.from_orm(hero) + db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) return db_hero -@app.get("/heroes/", response_model=List[HeroRead]) +@app.get("/heroes/", response_model=list[HeroPublic]) def read_heroes(): with Session(engine) as session: heroes = session.exec(select(Hero)).all() diff --git a/docs_src/tutorial/fastapi/read_one/tutorial001_py310.py b/docs_src/tutorial/fastapi/read_one/tutorial001_py310.py new file mode 100644 index 0000000000..1a4628137c --- /dev/null +++ b/docs_src/tutorial/fastapi/read_one/tutorial001_py310.py @@ -0,0 +1,65 @@ +from fastapi import FastAPI, HTTPException +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class HeroBase(SQLModel): + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +class Hero(HeroBase, table=True): + id: int | None = Field(default=None, primary_key=True) + + +class HeroCreate(HeroBase): + pass + + +class HeroPublic(HeroBase): + id: int + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +connect_args = {"check_same_thread": False} +engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +app = FastAPI() + + +@app.on_event("startup") +def on_startup(): + create_db_and_tables() + + +@app.post("/heroes/", response_model=HeroPublic) +def create_hero(hero: HeroCreate): + with Session(engine) as session: + db_hero = Hero.model_validate(hero) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.get("/heroes/", response_model=list[HeroPublic]) +def read_heroes(): + with Session(engine) as session: + heroes = session.exec(select(Hero)).all() + return heroes + + +@app.get("/heroes/{hero_id}", response_model=HeroPublic) +def read_hero(hero_id: int): + with Session(engine) as session: + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + return hero diff --git a/docs_src/tutorial/fastapi/read_one/tutorial001.py b/docs_src/tutorial/fastapi/read_one/tutorial001_py39.py similarity index 83% rename from docs_src/tutorial/fastapi/read_one/tutorial001.py rename to docs_src/tutorial/fastapi/read_one/tutorial001_py39.py index 4d66e471a5..9ac0a65088 100644 --- a/docs_src/tutorial/fastapi/read_one/tutorial001.py +++ b/docs_src/tutorial/fastapi/read_one/tutorial001_py39.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Optional from fastapi import FastAPI, HTTPException from sqlmodel import Field, Session, SQLModel, create_engine, select @@ -18,7 +18,7 @@ class HeroCreate(HeroBase): pass -class HeroRead(HeroBase): +class HeroPublic(HeroBase): id: int @@ -41,24 +41,24 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(hero: HeroCreate): with Session(engine) as session: - db_hero = Hero.from_orm(hero) + db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) return db_hero -@app.get("/heroes/", response_model=List[HeroRead]) +@app.get("/heroes/", response_model=list[HeroPublic]) def read_heroes(): with Session(engine) as session: heroes = session.exec(select(Hero)).all() return heroes -@app.get("/heroes/{hero_id}", response_model=HeroRead) +@app.get("/heroes/{hero_id}", response_model=HeroPublic) def read_hero(hero_id: int): with Session(engine) as session: hero = session.get(Hero, hero_id) diff --git a/docs_src/tutorial/fastapi/relationships/tutorial001_py310.py b/docs_src/tutorial/fastapi/relationships/tutorial001_py310.py new file mode 100644 index 0000000000..47c2e5f0bc --- /dev/null +++ b/docs_src/tutorial/fastapi/relationships/tutorial001_py310.py @@ -0,0 +1,197 @@ +from fastapi import Depends, FastAPI, HTTPException, Query +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select + + +class TeamBase(SQLModel): + name: str = Field(index=True) + headquarters: str + + +class Team(TeamBase, table=True): + id: int | None = Field(default=None, primary_key=True) + + heroes: list["Hero"] = Relationship(back_populates="team") + + +class TeamCreate(TeamBase): + pass + + +class TeamPublic(TeamBase): + id: int + + +class TeamUpdate(SQLModel): + id: int | None = None + name: str | None = None + headquarters: str | None = None + + +class HeroBase(SQLModel): + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id") + + +class Hero(HeroBase, table=True): + id: int | None = Field(default=None, primary_key=True) + + team: Team | None = Relationship(back_populates="heroes") + + +class HeroPublic(HeroBase): + id: int + + +class HeroCreate(HeroBase): + pass + + +class HeroUpdate(SQLModel): + name: str | None = None + secret_name: str | None = None + age: int | None = None + team_id: int | None = None + + +class HeroPublicWithTeam(HeroPublic): + team: TeamPublic | None = None + + +class TeamPublicWithHeroes(TeamPublic): + heroes: list[HeroPublic] = [] + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +connect_args = {"check_same_thread": False} +engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def get_session(): + with Session(engine) as session: + yield session + + +app = FastAPI() + + +@app.on_event("startup") +def on_startup(): + create_db_and_tables() + + +@app.post("/heroes/", response_model=HeroPublic) +def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): + db_hero = Hero.model_validate(hero) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.get("/heroes/", response_model=list[HeroPublic]) +def read_heroes( + *, + session: Session = Depends(get_session), + offset: int = 0, + limit: int = Query(default=100, le=100), +): + heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() + return heroes + + +@app.get("/heroes/{hero_id}", response_model=HeroPublicWithTeam) +def read_hero(*, session: Session = Depends(get_session), hero_id: int): + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + return hero + + +@app.patch("/heroes/{hero_id}", response_model=HeroPublic) +def update_hero( + *, session: Session = Depends(get_session), hero_id: int, hero: HeroUpdate +): + db_hero = session.get(Hero, hero_id) + if not db_hero: + raise HTTPException(status_code=404, detail="Hero not found") + hero_data = hero.model_dump(exclude_unset=True) + db_hero.sqlmodel_update(hero_data) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.delete("/heroes/{hero_id}") +def delete_hero(*, session: Session = Depends(get_session), hero_id: int): + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + session.delete(hero) + session.commit() + return {"ok": True} + + +@app.post("/teams/", response_model=TeamPublic) +def create_team(*, session: Session = Depends(get_session), team: TeamCreate): + db_team = Team.model_validate(team) + session.add(db_team) + session.commit() + session.refresh(db_team) + return db_team + + +@app.get("/teams/", response_model=list[TeamPublic]) +def read_teams( + *, + session: Session = Depends(get_session), + offset: int = 0, + limit: int = Query(default=100, le=100), +): + teams = session.exec(select(Team).offset(offset).limit(limit)).all() + return teams + + +@app.get("/teams/{team_id}", response_model=TeamPublicWithHeroes) +def read_team(*, team_id: int, session: Session = Depends(get_session)): + team = session.get(Team, team_id) + if not team: + raise HTTPException(status_code=404, detail="Team not found") + return team + + +@app.patch("/teams/{team_id}", response_model=TeamPublic) +def update_team( + *, + session: Session = Depends(get_session), + team_id: int, + team: TeamUpdate, +): + db_team = session.get(Team, team_id) + if not db_team: + raise HTTPException(status_code=404, detail="Team not found") + team_data = team.model_dump(exclude_unset=True) + db_team.sqlmodel_update(team_data) + session.add(db_team) + session.commit() + session.refresh(db_team) + return db_team + + +@app.delete("/teams/{team_id}") +def delete_team(*, session: Session = Depends(get_session), team_id: int): + team = session.get(Team, team_id) + if not team: + raise HTTPException(status_code=404, detail="Team not found") + session.delete(team) + session.commit() + return {"ok": True} diff --git a/docs_src/tutorial/fastapi/relationships/tutorial001.py b/docs_src/tutorial/fastapi/relationships/tutorial001_py39.py similarity index 77% rename from docs_src/tutorial/fastapi/relationships/tutorial001.py rename to docs_src/tutorial/fastapi/relationships/tutorial001_py39.py index 97220b95e5..1cfa298b8b 100644 --- a/docs_src/tutorial/fastapi/relationships/tutorial001.py +++ b/docs_src/tutorial/fastapi/relationships/tutorial001_py39.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Optional from fastapi import Depends, FastAPI, HTTPException, Query from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select @@ -12,14 +12,14 @@ class TeamBase(SQLModel): class Team(TeamBase, table=True): id: Optional[int] = Field(default=None, primary_key=True) - heroes: List["Hero"] = Relationship(back_populates="team") + heroes: list["Hero"] = Relationship(back_populates="team") class TeamCreate(TeamBase): pass -class TeamRead(TeamBase): +class TeamPublic(TeamBase): id: int @@ -43,7 +43,7 @@ class Hero(HeroBase, table=True): team: Optional[Team] = Relationship(back_populates="heroes") -class HeroRead(HeroBase): +class HeroPublic(HeroBase): id: int @@ -58,12 +58,12 @@ class HeroUpdate(SQLModel): team_id: Optional[int] = None -class HeroReadWithTeam(HeroRead): - team: Optional[TeamRead] = None +class HeroPublicWithTeam(HeroPublic): + team: Optional[TeamPublic] = None -class TeamReadWithHeroes(TeamRead): - heroes: List[HeroRead] = [] +class TeamPublicWithHeroes(TeamPublic): + heroes: list[HeroPublic] = [] sqlite_file_name = "database.db" @@ -90,27 +90,27 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): - db_hero = Hero.from_orm(hero) + db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) return db_hero -@app.get("/heroes/", response_model=List[HeroRead]) +@app.get("/heroes/", response_model=list[HeroPublic]) def read_heroes( *, session: Session = Depends(get_session), offset: int = 0, - limit: int = Query(default=100, lte=100), + limit: int = Query(default=100, le=100), ): heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() return heroes -@app.get("/heroes/{hero_id}", response_model=HeroReadWithTeam) +@app.get("/heroes/{hero_id}", response_model=HeroPublicWithTeam) def read_hero(*, session: Session = Depends(get_session), hero_id: int): hero = session.get(Hero, hero_id) if not hero: @@ -118,16 +118,15 @@ def read_hero(*, session: Session = Depends(get_session), hero_id: int): return hero -@app.patch("/heroes/{hero_id}", response_model=HeroRead) +@app.patch("/heroes/{hero_id}", response_model=HeroPublic) def update_hero( *, session: Session = Depends(get_session), hero_id: int, hero: HeroUpdate ): db_hero = session.get(Hero, hero_id) if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") - hero_data = hero.dict(exclude_unset=True) - for key, value in hero_data.items(): - setattr(db_hero, key, value) + hero_data = hero.model_dump(exclude_unset=True) + db_hero.sqlmodel_update(hero_data) session.add(db_hero) session.commit() session.refresh(db_hero) @@ -136,7 +135,6 @@ def update_hero( @app.delete("/heroes/{hero_id}") def delete_hero(*, session: Session = Depends(get_session), hero_id: int): - hero = session.get(Hero, hero_id) if not hero: raise HTTPException(status_code=404, detail="Hero not found") @@ -145,27 +143,27 @@ def delete_hero(*, session: Session = Depends(get_session), hero_id: int): return {"ok": True} -@app.post("/teams/", response_model=TeamRead) +@app.post("/teams/", response_model=TeamPublic) def create_team(*, session: Session = Depends(get_session), team: TeamCreate): - db_team = Team.from_orm(team) + db_team = Team.model_validate(team) session.add(db_team) session.commit() session.refresh(db_team) return db_team -@app.get("/teams/", response_model=List[TeamRead]) +@app.get("/teams/", response_model=list[TeamPublic]) def read_teams( *, session: Session = Depends(get_session), offset: int = 0, - limit: int = Query(default=100, lte=100), + limit: int = Query(default=100, le=100), ): teams = session.exec(select(Team).offset(offset).limit(limit)).all() return teams -@app.get("/teams/{team_id}", response_model=TeamReadWithHeroes) +@app.get("/teams/{team_id}", response_model=TeamPublicWithHeroes) def read_team(*, team_id: int, session: Session = Depends(get_session)): team = session.get(Team, team_id) if not team: @@ -173,7 +171,7 @@ def read_team(*, team_id: int, session: Session = Depends(get_session)): return team -@app.patch("/teams/{team_id}", response_model=TeamRead) +@app.patch("/teams/{team_id}", response_model=TeamPublic) def update_team( *, session: Session = Depends(get_session), @@ -183,9 +181,8 @@ def update_team( db_team = session.get(Team, team_id) if not db_team: raise HTTPException(status_code=404, detail="Team not found") - team_data = team.dict(exclude_unset=True) - for key, value in team_data.items(): - setattr(db_team, key, value) + team_data = team.model_dump(exclude_unset=True) + db_team.sqlmodel_update(team_data) session.add(db_team) session.commit() session.refresh(db_team) diff --git a/docs_src/tutorial/fastapi/response_model/tutorial001_py310.py b/docs_src/tutorial/fastapi/response_model/tutorial001_py310.py new file mode 100644 index 0000000000..25825b41ec --- /dev/null +++ b/docs_src/tutorial/fastapi/response_model/tutorial001_py310.py @@ -0,0 +1,44 @@ +from fastapi import FastAPI +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +connect_args = {"check_same_thread": False} +engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +app = FastAPI() + + +@app.on_event("startup") +def on_startup(): + create_db_and_tables() + + +@app.post("/heroes/", response_model=Hero) +def create_hero(hero: Hero): + with Session(engine) as session: + session.add(hero) + session.commit() + session.refresh(hero) + return hero + + +@app.get("/heroes/", response_model=list[Hero]) +def read_heroes(): + with Session(engine) as session: + heroes = session.exec(select(Hero)).all() + return heroes diff --git a/docs_src/tutorial/fastapi/response_model/tutorial001.py b/docs_src/tutorial/fastapi/response_model/tutorial001_py39.py similarity index 92% rename from docs_src/tutorial/fastapi/response_model/tutorial001.py rename to docs_src/tutorial/fastapi/response_model/tutorial001_py39.py index 57d8738395..53b701deb1 100644 --- a/docs_src/tutorial/fastapi/response_model/tutorial001.py +++ b/docs_src/tutorial/fastapi/response_model/tutorial001_py39.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Optional from fastapi import FastAPI from sqlmodel import Field, Session, SQLModel, create_engine, select @@ -39,7 +39,7 @@ def create_hero(hero: Hero): return hero -@app.get("/heroes/", response_model=List[Hero]) +@app.get("/heroes/", response_model=list[Hero]) def read_heroes(): with Session(engine) as session: heroes = session.exec(select(Hero)).all() diff --git a/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py b/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py new file mode 100644 index 0000000000..84da9fd610 --- /dev/null +++ b/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py @@ -0,0 +1,103 @@ +from fastapi import Depends, FastAPI, HTTPException, Query +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class HeroBase(SQLModel): + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +class Hero(HeroBase, table=True): + id: int | None = Field(default=None, primary_key=True) + + +class HeroCreate(HeroBase): + pass + + +class HeroPublic(HeroBase): + id: int + + +class HeroUpdate(SQLModel): + name: str | None = None + secret_name: str | None = None + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +connect_args = {"check_same_thread": False} +engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def get_session(): + with Session(engine) as session: + yield session + + +app = FastAPI() + + +@app.on_event("startup") +def on_startup(): + create_db_and_tables() + + +@app.post("/heroes/", response_model=HeroPublic) +def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): + db_hero = Hero.model_validate(hero) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.get("/heroes/", response_model=list[HeroPublic]) +def read_heroes( + *, + session: Session = Depends(get_session), + offset: int = 0, + limit: int = Query(default=100, le=100), +): + heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() + return heroes + + +@app.get("/heroes/{hero_id}", response_model=HeroPublic) +def read_hero(*, session: Session = Depends(get_session), hero_id: int): + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + return hero + + +@app.patch("/heroes/{hero_id}", response_model=HeroPublic) +def update_hero( + *, session: Session = Depends(get_session), hero_id: int, hero: HeroUpdate +): + db_hero = session.get(Hero, hero_id) + if not db_hero: + raise HTTPException(status_code=404, detail="Hero not found") + hero_data = hero.model_dump(exclude_unset=True) + db_hero.sqlmodel_update(hero_data) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.delete("/heroes/{hero_id}") +def delete_hero(*, session: Session = Depends(get_session), hero_id: int): + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + session.delete(hero) + session.commit() + return {"ok": True} diff --git a/docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py b/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py similarity index 82% rename from docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py rename to docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py index 88b8fbbcea..e7371d84e3 100644 --- a/docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py +++ b/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Optional from fastapi import Depends, FastAPI, HTTPException, Query from sqlmodel import Field, Session, SQLModel, create_engine, select @@ -18,7 +18,7 @@ class HeroCreate(HeroBase): pass -class HeroRead(HeroBase): +class HeroPublic(HeroBase): id: int @@ -52,27 +52,27 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): - db_hero = Hero.from_orm(hero) + db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) return db_hero -@app.get("/heroes/", response_model=List[HeroRead]) +@app.get("/heroes/", response_model=list[HeroPublic]) def read_heroes( *, session: Session = Depends(get_session), offset: int = 0, - limit: int = Query(default=100, lte=100), + limit: int = Query(default=100, le=100), ): heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() return heroes -@app.get("/heroes/{hero_id}", response_model=HeroRead) +@app.get("/heroes/{hero_id}", response_model=HeroPublic) def read_hero(*, session: Session = Depends(get_session), hero_id: int): hero = session.get(Hero, hero_id) if not hero: @@ -80,16 +80,15 @@ def read_hero(*, session: Session = Depends(get_session), hero_id: int): return hero -@app.patch("/heroes/{hero_id}", response_model=HeroRead) +@app.patch("/heroes/{hero_id}", response_model=HeroPublic) def update_hero( *, session: Session = Depends(get_session), hero_id: int, hero: HeroUpdate ): db_hero = session.get(Hero, hero_id) if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") - hero_data = hero.dict(exclude_unset=True) - for key, value in hero_data.items(): - setattr(db_hero, key, value) + hero_data = hero.model_dump(exclude_unset=True) + db_hero.sqlmodel_update(hero_data) session.add(db_hero) session.commit() session.refresh(db_hero) @@ -98,7 +97,6 @@ def update_hero( @app.delete("/heroes/{hero_id}") def delete_hero(*, session: Session = Depends(get_session), hero_id: int): - hero = session.get(Hero, hero_id) if not hero: raise HTTPException(status_code=404, detail="Hero not found") diff --git a/docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py310.py b/docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py310.py new file mode 100644 index 0000000000..0e113b0f16 --- /dev/null +++ b/docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py310.py @@ -0,0 +1,44 @@ +from fastapi import FastAPI +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +connect_args = {"check_same_thread": False} +engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +app = FastAPI() + + +@app.on_event("startup") +def on_startup(): + create_db_and_tables() + + +@app.post("/heroes/") +def create_hero(hero: Hero): + with Session(engine) as session: + session.add(hero) + session.commit() + session.refresh(hero) + return hero + + +@app.get("/heroes/") +def read_heroes(): + with Session(engine) as session: + heroes = session.exec(select(Hero)).all() + return heroes diff --git a/docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py b/docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py39.py similarity index 100% rename from docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py rename to docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py39.py diff --git a/docs_src/tutorial/fastapi/teams/tutorial001_py310.py b/docs_src/tutorial/fastapi/teams/tutorial001_py310.py new file mode 100644 index 0000000000..b78f059e12 --- /dev/null +++ b/docs_src/tutorial/fastapi/teams/tutorial001_py310.py @@ -0,0 +1,188 @@ +from fastapi import Depends, FastAPI, HTTPException, Query +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select + + +class TeamBase(SQLModel): + name: str = Field(index=True) + headquarters: str + + +class Team(TeamBase, table=True): + id: int | None = Field(default=None, primary_key=True) + + heroes: list["Hero"] = Relationship(back_populates="team") + + +class TeamCreate(TeamBase): + pass + + +class TeamPublic(TeamBase): + id: int + + +class TeamUpdate(SQLModel): + name: str | None = None + headquarters: str | None = None + + +class HeroBase(SQLModel): + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id") + + +class Hero(HeroBase, table=True): + id: int | None = Field(default=None, primary_key=True) + + team: Team | None = Relationship(back_populates="heroes") + + +class HeroPublic(HeroBase): + id: int + + +class HeroCreate(HeroBase): + pass + + +class HeroUpdate(SQLModel): + name: str | None = None + secret_name: str | None = None + age: int | None = None + team_id: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +connect_args = {"check_same_thread": False} +engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def get_session(): + with Session(engine) as session: + yield session + + +app = FastAPI() + + +@app.on_event("startup") +def on_startup(): + create_db_and_tables() + + +@app.post("/heroes/", response_model=HeroPublic) +def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): + db_hero = Hero.model_validate(hero) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.get("/heroes/", response_model=list[HeroPublic]) +def read_heroes( + *, + session: Session = Depends(get_session), + offset: int = 0, + limit: int = Query(default=100, le=100), +): + heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() + return heroes + + +@app.get("/heroes/{hero_id}", response_model=HeroPublic) +def read_hero(*, session: Session = Depends(get_session), hero_id: int): + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + return hero + + +@app.patch("/heroes/{hero_id}", response_model=HeroPublic) +def update_hero( + *, session: Session = Depends(get_session), hero_id: int, hero: HeroUpdate +): + db_hero = session.get(Hero, hero_id) + if not db_hero: + raise HTTPException(status_code=404, detail="Hero not found") + hero_data = hero.model_dump(exclude_unset=True) + db_hero.sqlmodel_update(hero_data) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.delete("/heroes/{hero_id}") +def delete_hero(*, session: Session = Depends(get_session), hero_id: int): + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + session.delete(hero) + session.commit() + return {"ok": True} + + +@app.post("/teams/", response_model=TeamPublic) +def create_team(*, session: Session = Depends(get_session), team: TeamCreate): + db_team = Team.model_validate(team) + session.add(db_team) + session.commit() + session.refresh(db_team) + return db_team + + +@app.get("/teams/", response_model=list[TeamPublic]) +def read_teams( + *, + session: Session = Depends(get_session), + offset: int = 0, + limit: int = Query(default=100, le=100), +): + teams = session.exec(select(Team).offset(offset).limit(limit)).all() + return teams + + +@app.get("/teams/{team_id}", response_model=TeamPublic) +def read_team(*, team_id: int, session: Session = Depends(get_session)): + team = session.get(Team, team_id) + if not team: + raise HTTPException(status_code=404, detail="Team not found") + return team + + +@app.patch("/teams/{team_id}", response_model=TeamPublic) +def update_team( + *, + session: Session = Depends(get_session), + team_id: int, + team: TeamUpdate, +): + db_team = session.get(Team, team_id) + if not db_team: + raise HTTPException(status_code=404, detail="Team not found") + team_data = team.model_dump(exclude_unset=True) + db_team.sqlmodel_update(team_data) + session.add(db_team) + session.commit() + session.refresh(db_team) + return db_team + + +@app.delete("/teams/{team_id}") +def delete_team(*, session: Session = Depends(get_session), team_id: int): + team = session.get(Team, team_id) + if not team: + raise HTTPException(status_code=404, detail="Team not found") + session.delete(team) + session.commit() + return {"ok": True} diff --git a/docs_src/tutorial/fastapi/teams/tutorial001.py b/docs_src/tutorial/fastapi/teams/tutorial001_py39.py similarity index 79% rename from docs_src/tutorial/fastapi/teams/tutorial001.py rename to docs_src/tutorial/fastapi/teams/tutorial001_py39.py index e8f88b8e9e..928ec70976 100644 --- a/docs_src/tutorial/fastapi/teams/tutorial001.py +++ b/docs_src/tutorial/fastapi/teams/tutorial001_py39.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Optional from fastapi import Depends, FastAPI, HTTPException, Query from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select @@ -12,19 +12,18 @@ class TeamBase(SQLModel): class Team(TeamBase, table=True): id: Optional[int] = Field(default=None, primary_key=True) - heroes: List["Hero"] = Relationship(back_populates="team") + heroes: list["Hero"] = Relationship(back_populates="team") class TeamCreate(TeamBase): pass -class TeamRead(TeamBase): +class TeamPublic(TeamBase): id: int class TeamUpdate(SQLModel): - id: Optional[int] = None name: Optional[str] = None headquarters: Optional[str] = None @@ -43,7 +42,7 @@ class Hero(HeroBase, table=True): team: Optional[Team] = Relationship(back_populates="heroes") -class HeroRead(HeroBase): +class HeroPublic(HeroBase): id: int @@ -82,27 +81,27 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): - db_hero = Hero.from_orm(hero) + db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) return db_hero -@app.get("/heroes/", response_model=List[HeroRead]) +@app.get("/heroes/", response_model=list[HeroPublic]) def read_heroes( *, session: Session = Depends(get_session), offset: int = 0, - limit: int = Query(default=100, lte=100), + limit: int = Query(default=100, le=100), ): heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() return heroes -@app.get("/heroes/{hero_id}", response_model=HeroRead) +@app.get("/heroes/{hero_id}", response_model=HeroPublic) def read_hero(*, session: Session = Depends(get_session), hero_id: int): hero = session.get(Hero, hero_id) if not hero: @@ -110,16 +109,15 @@ def read_hero(*, session: Session = Depends(get_session), hero_id: int): return hero -@app.patch("/heroes/{hero_id}", response_model=HeroRead) +@app.patch("/heroes/{hero_id}", response_model=HeroPublic) def update_hero( *, session: Session = Depends(get_session), hero_id: int, hero: HeroUpdate ): db_hero = session.get(Hero, hero_id) if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") - hero_data = hero.dict(exclude_unset=True) - for key, value in hero_data.items(): - setattr(db_hero, key, value) + hero_data = hero.model_dump(exclude_unset=True) + db_hero.sqlmodel_update(hero_data) session.add(db_hero) session.commit() session.refresh(db_hero) @@ -128,7 +126,6 @@ def update_hero( @app.delete("/heroes/{hero_id}") def delete_hero(*, session: Session = Depends(get_session), hero_id: int): - hero = session.get(Hero, hero_id) if not hero: raise HTTPException(status_code=404, detail="Hero not found") @@ -137,27 +134,27 @@ def delete_hero(*, session: Session = Depends(get_session), hero_id: int): return {"ok": True} -@app.post("/teams/", response_model=TeamRead) +@app.post("/teams/", response_model=TeamPublic) def create_team(*, session: Session = Depends(get_session), team: TeamCreate): - db_team = Team.from_orm(team) + db_team = Team.model_validate(team) session.add(db_team) session.commit() session.refresh(db_team) return db_team -@app.get("/teams/", response_model=List[TeamRead]) +@app.get("/teams/", response_model=list[TeamPublic]) def read_teams( *, session: Session = Depends(get_session), offset: int = 0, - limit: int = Query(default=100, lte=100), + limit: int = Query(default=100, le=100), ): teams = session.exec(select(Team).offset(offset).limit(limit)).all() return teams -@app.get("/teams/{team_id}", response_model=TeamRead) +@app.get("/teams/{team_id}", response_model=TeamPublic) def read_team(*, team_id: int, session: Session = Depends(get_session)): team = session.get(Team, team_id) if not team: @@ -165,7 +162,7 @@ def read_team(*, team_id: int, session: Session = Depends(get_session)): return team -@app.patch("/teams/{team_id}", response_model=TeamRead) +@app.patch("/teams/{team_id}", response_model=TeamPublic) def update_team( *, session: Session = Depends(get_session), @@ -175,9 +172,8 @@ def update_team( db_team = session.get(Team, team_id) if not db_team: raise HTTPException(status_code=404, detail="Team not found") - team_data = team.dict(exclude_unset=True) - for key, value in team_data.items(): - setattr(db_team, key, value) + team_data = team.model_dump(exclude_unset=True) + db_team.sqlmodel_update(team_data) session.add(db_team) session.commit() session.refresh(db_team) diff --git a/docs_src/tutorial/fastapi/update/tutorial001_py310.py b/docs_src/tutorial/fastapi/update/tutorial001_py310.py new file mode 100644 index 0000000000..a98ee68fbb --- /dev/null +++ b/docs_src/tutorial/fastapi/update/tutorial001_py310.py @@ -0,0 +1,85 @@ +from fastapi import FastAPI, HTTPException, Query +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class HeroBase(SQLModel): + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +class Hero(HeroBase, table=True): + id: int | None = Field(default=None, primary_key=True) + + +class HeroCreate(HeroBase): + pass + + +class HeroPublic(HeroBase): + id: int + + +class HeroUpdate(SQLModel): + name: str | None = None + secret_name: str | None = None + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +connect_args = {"check_same_thread": False} +engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +app = FastAPI() + + +@app.on_event("startup") +def on_startup(): + create_db_and_tables() + + +@app.post("/heroes/", response_model=HeroPublic) +def create_hero(hero: HeroCreate): + with Session(engine) as session: + db_hero = Hero.model_validate(hero) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.get("/heroes/", response_model=list[HeroPublic]) +def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)): + with Session(engine) as session: + heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() + return heroes + + +@app.get("/heroes/{hero_id}", response_model=HeroPublic) +def read_hero(hero_id: int): + with Session(engine) as session: + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + return hero + + +@app.patch("/heroes/{hero_id}", response_model=HeroPublic) +def update_hero(hero_id: int, hero: HeroUpdate): + with Session(engine) as session: + db_hero = session.get(Hero, hero_id) + if not db_hero: + raise HTTPException(status_code=404, detail="Hero not found") + hero_data = hero.model_dump(exclude_unset=True) + db_hero.sqlmodel_update(hero_data) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero diff --git a/docs_src/tutorial/fastapi/update/tutorial001.py b/docs_src/tutorial/fastapi/update/tutorial001_py39.py similarity index 80% rename from docs_src/tutorial/fastapi/update/tutorial001.py rename to docs_src/tutorial/fastapi/update/tutorial001_py39.py index 35554878db..b6d62bf81c 100644 --- a/docs_src/tutorial/fastapi/update/tutorial001.py +++ b/docs_src/tutorial/fastapi/update/tutorial001_py39.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Optional from fastapi import FastAPI, HTTPException, Query from sqlmodel import Field, Session, SQLModel, create_engine, select @@ -18,7 +18,7 @@ class HeroCreate(HeroBase): pass -class HeroRead(HeroBase): +class HeroPublic(HeroBase): id: int @@ -47,24 +47,24 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(hero: HeroCreate): with Session(engine) as session: - db_hero = Hero.from_orm(hero) + db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) return db_hero -@app.get("/heroes/", response_model=List[HeroRead]) -def read_heroes(offset: int = 0, limit: int = Query(default=100, lte=100)): +@app.get("/heroes/", response_model=list[HeroPublic]) +def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)): with Session(engine) as session: heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() return heroes -@app.get("/heroes/{hero_id}", response_model=HeroRead) +@app.get("/heroes/{hero_id}", response_model=HeroPublic) def read_hero(hero_id: int): with Session(engine) as session: hero = session.get(Hero, hero_id) @@ -73,15 +73,14 @@ def read_hero(hero_id: int): return hero -@app.patch("/heroes/{hero_id}", response_model=HeroRead) +@app.patch("/heroes/{hero_id}", response_model=HeroPublic) def update_hero(hero_id: int, hero: HeroUpdate): with Session(engine) as session: db_hero = session.get(Hero, hero_id) if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") - hero_data = hero.dict(exclude_unset=True) - for key, value in hero_data.items(): - setattr(db_hero, key, value) + hero_data = hero.model_dump(exclude_unset=True) + db_hero.sqlmodel_update(hero_data) session.add(db_hero) session.commit() session.refresh(db_hero) diff --git a/docs_src/tutorial/fastapi/update/tutorial002_py310.py b/docs_src/tutorial/fastapi/update/tutorial002_py310.py new file mode 100644 index 0000000000..d250fecf3a --- /dev/null +++ b/docs_src/tutorial/fastapi/update/tutorial002_py310.py @@ -0,0 +1,99 @@ +from fastapi import FastAPI, HTTPException, Query +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class HeroBase(SQLModel): + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +class Hero(HeroBase, table=True): + id: int | None = Field(default=None, primary_key=True) + hashed_password: str = Field() + + +class HeroCreate(HeroBase): + password: str + + +class HeroPublic(HeroBase): + id: int + + +class HeroUpdate(SQLModel): + name: str | None = None + secret_name: str | None = None + age: int | None = None + password: str | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +connect_args = {"check_same_thread": False} +engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def hash_password(password: str) -> str: + # Use something like passlib here + return f"not really hashed {password} hehehe" + + +app = FastAPI() + + +@app.on_event("startup") +def on_startup(): + create_db_and_tables() + + +@app.post("/heroes/", response_model=HeroPublic) +def create_hero(hero: HeroCreate): + hashed_password = hash_password(hero.password) + with Session(engine) as session: + extra_data = {"hashed_password": hashed_password} + db_hero = Hero.model_validate(hero, update=extra_data) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.get("/heroes/", response_model=list[HeroPublic]) +def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)): + with Session(engine) as session: + heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() + return heroes + + +@app.get("/heroes/{hero_id}", response_model=HeroPublic) +def read_hero(hero_id: int): + with Session(engine) as session: + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + return hero + + +@app.patch("/heroes/{hero_id}", response_model=HeroPublic) +def update_hero(hero_id: int, hero: HeroUpdate): + with Session(engine) as session: + db_hero = session.get(Hero, hero_id) + if not db_hero: + raise HTTPException(status_code=404, detail="Hero not found") + hero_data = hero.model_dump(exclude_unset=True) + extra_data = {} + if "password" in hero_data: + password = hero_data["password"] + hashed_password = hash_password(password) + extra_data["hashed_password"] = hashed_password + db_hero.sqlmodel_update(hero_data, update=extra_data) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero diff --git a/docs_src/tutorial/fastapi/update/tutorial002_py39.py b/docs_src/tutorial/fastapi/update/tutorial002_py39.py new file mode 100644 index 0000000000..14ad1b4826 --- /dev/null +++ b/docs_src/tutorial/fastapi/update/tutorial002_py39.py @@ -0,0 +1,101 @@ +from typing import Optional + +from fastapi import FastAPI, HTTPException, Query +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class HeroBase(SQLModel): + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + +class Hero(HeroBase, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + hashed_password: str = Field() + + +class HeroCreate(HeroBase): + password: str + + +class HeroPublic(HeroBase): + id: int + + +class HeroUpdate(SQLModel): + name: Optional[str] = None + secret_name: Optional[str] = None + age: Optional[int] = None + password: Optional[str] = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +connect_args = {"check_same_thread": False} +engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def hash_password(password: str) -> str: + # Use something like passlib here + return f"not really hashed {password} hehehe" + + +app = FastAPI() + + +@app.on_event("startup") +def on_startup(): + create_db_and_tables() + + +@app.post("/heroes/", response_model=HeroPublic) +def create_hero(hero: HeroCreate): + hashed_password = hash_password(hero.password) + with Session(engine) as session: + extra_data = {"hashed_password": hashed_password} + db_hero = Hero.model_validate(hero, update=extra_data) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.get("/heroes/", response_model=list[HeroPublic]) +def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)): + with Session(engine) as session: + heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() + return heroes + + +@app.get("/heroes/{hero_id}", response_model=HeroPublic) +def read_hero(hero_id: int): + with Session(engine) as session: + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + return hero + + +@app.patch("/heroes/{hero_id}", response_model=HeroPublic) +def update_hero(hero_id: int, hero: HeroUpdate): + with Session(engine) as session: + db_hero = session.get(Hero, hero_id) + if not db_hero: + raise HTTPException(status_code=404, detail="Hero not found") + hero_data = hero.model_dump(exclude_unset=True) + extra_data = {} + if "password" in hero_data: + password = hero_data["password"] + hashed_password = hash_password(password) + extra_data["hashed_password"] = hashed_password + db_hero.sqlmodel_update(hero_data, update=extra_data) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero diff --git a/docs_src/tutorial/indexes/tutorial001_py310.py b/docs_src/tutorial/indexes/tutorial001_py310.py new file mode 100644 index 0000000000..115c447bcf --- /dev/null +++ b/docs_src/tutorial/indexes/tutorial001_py310.py @@ -0,0 +1,49 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Deadpond") + results = session.exec(statement) + for hero in results: + print(hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/indexes/tutorial001.py b/docs_src/tutorial/indexes/tutorial001_py39.py similarity index 100% rename from docs_src/tutorial/indexes/tutorial001.py rename to docs_src/tutorial/indexes/tutorial001_py39.py diff --git a/docs_src/tutorial/indexes/tutorial002_py310.py b/docs_src/tutorial/indexes/tutorial002_py310.py new file mode 100644 index 0000000000..c0b7a1e65e --- /dev/null +++ b/docs_src/tutorial/indexes/tutorial002_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.age <= 35) + results = session.exec(statement) + for hero in results: + print(hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/indexes/tutorial002.py b/docs_src/tutorial/indexes/tutorial002_py39.py similarity index 100% rename from docs_src/tutorial/indexes/tutorial002.py rename to docs_src/tutorial/indexes/tutorial002_py39.py diff --git a/docs_src/tutorial/insert/tutorial001_py310.py b/docs_src/tutorial/insert/tutorial001_py310.py new file mode 100644 index 0000000000..72f95ee279 --- /dev/null +++ b/docs_src/tutorial/insert/tutorial001_py310.py @@ -0,0 +1,43 @@ +from sqlmodel import Field, Session, SQLModel, create_engine + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + + session = Session(engine) + + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + + session.commit() + + session.close() + + +def main(): + create_db_and_tables() + create_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/insert/tutorial001.py b/docs_src/tutorial/insert/tutorial001_py39.py similarity index 100% rename from docs_src/tutorial/insert/tutorial001.py rename to docs_src/tutorial/insert/tutorial001_py39.py diff --git a/docs_src/tutorial/insert/tutorial002_py310.py b/docs_src/tutorial/insert/tutorial002_py310.py new file mode 100644 index 0000000000..266fbb8d32 --- /dev/null +++ b/docs_src/tutorial/insert/tutorial002_py310.py @@ -0,0 +1,40 @@ +from sqlmodel import Field, Session, SQLModel, create_engine + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + + session.commit() + + +def main(): + create_db_and_tables() + create_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/insert/tutorial002.py b/docs_src/tutorial/insert/tutorial002_py39.py similarity index 100% rename from docs_src/tutorial/insert/tutorial002.py rename to docs_src/tutorial/insert/tutorial002_py39.py diff --git a/docs_src/tutorial/insert/tutorial003_py310.py b/docs_src/tutorial/insert/tutorial003_py310.py new file mode 100644 index 0000000000..da94841aba --- /dev/null +++ b/docs_src/tutorial/insert/tutorial003_py310.py @@ -0,0 +1,41 @@ +from sqlmodel import Field, Session, SQLModel, create_engine + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): # (1)! + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") # (2)! + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + + with Session(engine) as session: # (3)! + session.add(hero_1) # (4)! + session.add(hero_2) + session.add(hero_3) + + session.commit() # (5)! + # (6)! + + +def main(): # (7)! + create_db_and_tables() # (8)! + create_heroes() # (9)! + + +if __name__ == "__main__": # (10)! + main() # (11)! diff --git a/docs_src/tutorial/insert/tutorial003.py b/docs_src/tutorial/insert/tutorial003_py39.py similarity index 70% rename from docs_src/tutorial/insert/tutorial003.py rename to docs_src/tutorial/insert/tutorial003_py39.py index 8133f2901b..03f51d2359 100644 --- a/docs_src/tutorial/insert/tutorial003.py +++ b/docs_src/tutorial/insert/tutorial003_py39.py @@ -20,24 +20,24 @@ def create_db_and_tables(): SQLModel.metadata.create_all(engine) -def create_heroes(): # (1) - hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") # (2) +def create_heroes(): # (1)! + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") # (2)! hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) - with Session(engine) as session: # (3) - session.add(hero_1) # (4) + with Session(engine) as session: # (3)! + session.add(hero_1) # (4)! session.add(hero_2) session.add(hero_3) - session.commit() # (5) - # (6) + session.commit() # (5)! + # (6)! -def main(): # (7) - create_db_and_tables() # (8) - create_heroes() # (9) +def main(): # (7)! + create_db_and_tables() # (8)! + create_heroes() # (9)! -if __name__ == "__main__": # (10) - main() # (11) +if __name__ == "__main__": # (10)! + main() # (11)! diff --git a/docs_src/tutorial/many_to_many/tutorial001_py310.py b/docs_src/tutorial/many_to_many/tutorial001_py310.py new file mode 100644 index 0000000000..e47d3920eb --- /dev/null +++ b/docs_src/tutorial/many_to_many/tutorial001_py310.py @@ -0,0 +1,78 @@ +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine + + +class HeroTeamLink(SQLModel, table=True): + team_id: int | None = Field(default=None, foreign_key="team.id", primary_key=True) + hero_id: int | None = Field(default=None, foreign_key="hero.id", primary_key=True) + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="teams", link_model=HeroTeamLink) + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + teams: list[Team] = Relationship(back_populates="heroes", link_model=HeroTeamLink) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + + hero_deadpond = Hero( + name="Deadpond", + secret_name="Dive Wilson", + teams=[team_z_force, team_preventers], + ) + hero_rusty_man = Hero( + name="Rusty-Man", + secret_name="Tommy Sharp", + age=48, + teams=[team_preventers], + ) + hero_spider_boy = Hero( + name="Spider-Boy", secret_name="Pedro Parqueador", teams=[team_preventers] + ) + session.add(hero_deadpond) + session.add(hero_rusty_man) + session.add(hero_spider_boy) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + session.refresh(hero_spider_boy) + + print("Deadpond:", hero_deadpond) + print("Deadpond teams:", hero_deadpond.teams) + print("Rusty-Man:", hero_rusty_man) + print("Rusty-Man Teams:", hero_rusty_man.teams) + print("Spider-Boy:", hero_spider_boy) + print("Spider-Boy Teams:", hero_spider_boy.teams) + + +def main(): + create_db_and_tables() + create_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/many_to_many/tutorial001.py b/docs_src/tutorial/many_to_many/tutorial001_py39.py similarity index 92% rename from docs_src/tutorial/many_to_many/tutorial001.py rename to docs_src/tutorial/many_to_many/tutorial001_py39.py index bb4e9d0896..c39fe91452 100644 --- a/docs_src/tutorial/many_to_many/tutorial001.py +++ b/docs_src/tutorial/many_to_many/tutorial001_py39.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Optional from sqlmodel import Field, Relationship, Session, SQLModel, create_engine @@ -17,7 +17,7 @@ class Team(SQLModel, table=True): name: str = Field(index=True) headquarters: str - heroes: List["Hero"] = Relationship(back_populates="teams", link_model=HeroTeamLink) + heroes: list["Hero"] = Relationship(back_populates="teams", link_model=HeroTeamLink) class Hero(SQLModel, table=True): @@ -26,7 +26,7 @@ class Hero(SQLModel, table=True): secret_name: str age: Optional[int] = Field(default=None, index=True) - teams: List[Team] = Relationship(back_populates="heroes", link_model=HeroTeamLink) + teams: list[Team] = Relationship(back_populates="heroes", link_model=HeroTeamLink) sqlite_file_name = "database.db" @@ -42,7 +42,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", diff --git a/docs_src/tutorial/many_to_many/tutorial002_py310.py b/docs_src/tutorial/many_to_many/tutorial002_py310.py new file mode 100644 index 0000000000..2668161e92 --- /dev/null +++ b/docs_src/tutorial/many_to_many/tutorial002_py310.py @@ -0,0 +1,101 @@ +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select + + +class HeroTeamLink(SQLModel, table=True): + team_id: int | None = Field(default=None, foreign_key="team.id", primary_key=True) + hero_id: int | None = Field(default=None, foreign_key="hero.id", primary_key=True) + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="teams", link_model=HeroTeamLink) + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + teams: list[Team] = Relationship(back_populates="heroes", link_model=HeroTeamLink) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + + hero_deadpond = Hero( + name="Deadpond", + secret_name="Dive Wilson", + teams=[team_z_force, team_preventers], + ) + hero_rusty_man = Hero( + name="Rusty-Man", + secret_name="Tommy Sharp", + age=48, + teams=[team_preventers], + ) + hero_spider_boy = Hero( + name="Spider-Boy", secret_name="Pedro Parqueador", teams=[team_preventers] + ) + session.add(hero_deadpond) + session.add(hero_rusty_man) + session.add(hero_spider_boy) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + session.refresh(hero_spider_boy) + + print("Deadpond:", hero_deadpond) + print("Deadpond teams:", hero_deadpond.teams) + print("Rusty-Man:", hero_rusty_man) + print("Rusty-Man Teams:", hero_rusty_man.teams) + print("Spider-Boy:", hero_spider_boy) + print("Spider-Boy Teams:", hero_spider_boy.teams) + + +def update_heroes(): + with Session(engine) as session: + hero_spider_boy = session.exec( + select(Hero).where(Hero.name == "Spider-Boy") + ).one() + team_z_force = session.exec(select(Team).where(Team.name == "Z-Force")).one() + + team_z_force.heroes.append(hero_spider_boy) + session.add(team_z_force) + session.commit() + + print("Updated Spider-Boy's Teams:", hero_spider_boy.teams) + print("Z-Force heroes:", team_z_force.heroes) + + hero_spider_boy.teams.remove(team_z_force) + session.add(team_z_force) + session.commit() + + print("Reverted Z-Force's heroes:", team_z_force.heroes) + print("Reverted Spider-Boy's teams:", hero_spider_boy.teams) + + +def main(): + create_db_and_tables() + create_heroes() + update_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/many_to_many/tutorial002.py b/docs_src/tutorial/many_to_many/tutorial002_py39.py similarity index 94% rename from docs_src/tutorial/many_to_many/tutorial002.py rename to docs_src/tutorial/many_to_many/tutorial002_py39.py index dc4aa0b770..c3b5f88f0e 100644 --- a/docs_src/tutorial/many_to_many/tutorial002.py +++ b/docs_src/tutorial/many_to_many/tutorial002_py39.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Optional from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select @@ -17,7 +17,7 @@ class Team(SQLModel, table=True): name: str = Field(index=True) headquarters: str - heroes: List["Hero"] = Relationship(back_populates="teams", link_model=HeroTeamLink) + heroes: list["Hero"] = Relationship(back_populates="teams", link_model=HeroTeamLink) class Hero(SQLModel, table=True): @@ -26,7 +26,7 @@ class Hero(SQLModel, table=True): secret_name: str age: Optional[int] = Field(default=None, index=True) - teams: List[Team] = Relationship(back_populates="heroes", link_model=HeroTeamLink) + teams: list[Team] = Relationship(back_populates="heroes", link_model=HeroTeamLink) sqlite_file_name = "database.db" @@ -42,7 +42,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", diff --git a/docs_src/tutorial/many_to_many/tutorial003_py310.py b/docs_src/tutorial/many_to_many/tutorial003_py310.py new file mode 100644 index 0000000000..4c1ad12259 --- /dev/null +++ b/docs_src/tutorial/many_to_many/tutorial003_py310.py @@ -0,0 +1,117 @@ +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select + + +class HeroTeamLink(SQLModel, table=True): + team_id: int | None = Field(default=None, foreign_key="team.id", primary_key=True) + hero_id: int | None = Field(default=None, foreign_key="hero.id", primary_key=True) + is_training: bool = False + + team: "Team" = Relationship(back_populates="hero_links") + hero: "Hero" = Relationship(back_populates="team_links") + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + hero_links: list[HeroTeamLink] = Relationship(back_populates="team") + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_links: list[HeroTeamLink] = Relationship(back_populates="hero") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + + hero_deadpond = Hero( + name="Deadpond", + secret_name="Dive Wilson", + ) + hero_rusty_man = Hero( + name="Rusty-Man", + secret_name="Tommy Sharp", + age=48, + ) + hero_spider_boy = Hero( + name="Spider-Boy", + secret_name="Pedro Parqueador", + ) + deadpond_team_z_link = HeroTeamLink(team=team_z_force, hero=hero_deadpond) + deadpond_preventers_link = HeroTeamLink( + team=team_preventers, hero=hero_deadpond, is_training=True + ) + spider_boy_preventers_link = HeroTeamLink( + team=team_preventers, hero=hero_spider_boy, is_training=True + ) + rusty_man_preventers_link = HeroTeamLink( + team=team_preventers, hero=hero_rusty_man + ) + + session.add(deadpond_team_z_link) + session.add(deadpond_preventers_link) + session.add(spider_boy_preventers_link) + session.add(rusty_man_preventers_link) + session.commit() + + for link in team_z_force.hero_links: + print("Z-Force hero:", link.hero, "is training:", link.is_training) + + for link in team_preventers.hero_links: + print("Preventers hero:", link.hero, "is training:", link.is_training) + + +def update_heroes(): + with Session(engine) as session: + hero_spider_boy = session.exec( + select(Hero).where(Hero.name == "Spider-Boy") + ).one() + team_z_force = session.exec(select(Team).where(Team.name == "Z-Force")).one() + + spider_boy_z_force_link = HeroTeamLink( + team=team_z_force, hero=hero_spider_boy, is_training=True + ) + team_z_force.hero_links.append(spider_boy_z_force_link) + session.add(team_z_force) + session.commit() + + print("Updated Spider-Boy's Teams:", hero_spider_boy.team_links) + print("Z-Force heroes:", team_z_force.hero_links) + + for link in hero_spider_boy.team_links: + if link.team.name == "Preventers": + link.is_training = False + + session.add(hero_spider_boy) + session.commit() + + for link in hero_spider_boy.team_links: + print("Spider-Boy team:", link.team, "is training:", link.is_training) + + +def main(): + create_db_and_tables() + create_heroes() + update_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/many_to_many/tutorial003.py b/docs_src/tutorial/many_to_many/tutorial003_py39.py similarity index 95% rename from docs_src/tutorial/many_to_many/tutorial003.py rename to docs_src/tutorial/many_to_many/tutorial003_py39.py index 1e03c4af89..175fbf318b 100644 --- a/docs_src/tutorial/many_to_many/tutorial003.py +++ b/docs_src/tutorial/many_to_many/tutorial003_py39.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Optional from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select @@ -21,7 +21,7 @@ class Team(SQLModel, table=True): name: str = Field(index=True) headquarters: str - hero_links: List[HeroTeamLink] = Relationship(back_populates="team") + hero_links: list[HeroTeamLink] = Relationship(back_populates="team") class Hero(SQLModel, table=True): @@ -30,7 +30,7 @@ class Hero(SQLModel, table=True): secret_name: str age: Optional[int] = Field(default=None, index=True) - team_links: List[HeroTeamLink] = Relationship(back_populates="hero") + team_links: list[HeroTeamLink] = Relationship(back_populates="hero") sqlite_file_name = "database.db" @@ -46,7 +46,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", diff --git a/docs_src/tutorial/offset_and_limit/tutorial001_py310.py b/docs_src/tutorial/offset_and_limit/tutorial001_py310.py new file mode 100644 index 0000000000..931f46e247 --- /dev/null +++ b/docs_src/tutorial/offset_and_limit/tutorial001_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).limit(3) + results = session.exec(statement) + heroes = results.all() + print(heroes) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/offset_and_limit/tutorial001.py b/docs_src/tutorial/offset_and_limit/tutorial001_py39.py similarity index 100% rename from docs_src/tutorial/offset_and_limit/tutorial001.py rename to docs_src/tutorial/offset_and_limit/tutorial001_py39.py diff --git a/docs_src/tutorial/offset_and_limit/tutorial002_py310.py b/docs_src/tutorial/offset_and_limit/tutorial002_py310.py new file mode 100644 index 0000000000..ab5c89b7cc --- /dev/null +++ b/docs_src/tutorial/offset_and_limit/tutorial002_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).offset(3).limit(3) + results = session.exec(statement) + heroes = results.all() + print(heroes) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/offset_and_limit/tutorial002.py b/docs_src/tutorial/offset_and_limit/tutorial002_py39.py similarity index 100% rename from docs_src/tutorial/offset_and_limit/tutorial002.py rename to docs_src/tutorial/offset_and_limit/tutorial002_py39.py diff --git a/docs_src/tutorial/offset_and_limit/tutorial003_py310.py b/docs_src/tutorial/offset_and_limit/tutorial003_py310.py new file mode 100644 index 0000000000..5ac24937d6 --- /dev/null +++ b/docs_src/tutorial/offset_and_limit/tutorial003_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).offset(6).limit(3) + results = session.exec(statement) + heroes = results.all() + print(heroes) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/offset_and_limit/tutorial003.py b/docs_src/tutorial/offset_and_limit/tutorial003_py39.py similarity index 100% rename from docs_src/tutorial/offset_and_limit/tutorial003.py rename to docs_src/tutorial/offset_and_limit/tutorial003_py39.py diff --git a/docs_src/tutorial/offset_and_limit/tutorial004_py310.py b/docs_src/tutorial/offset_and_limit/tutorial004_py310.py new file mode 100644 index 0000000000..c7e7bbb414 --- /dev/null +++ b/docs_src/tutorial/offset_and_limit/tutorial004_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.age > 32).offset(1).limit(2) + results = session.exec(statement) + heroes = results.all() + print(heroes) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/offset_and_limit/tutorial004.py b/docs_src/tutorial/offset_and_limit/tutorial004_py39.py similarity index 95% rename from docs_src/tutorial/offset_and_limit/tutorial004.py rename to docs_src/tutorial/offset_and_limit/tutorial004_py39.py index a95715cd98..43828b853f 100644 --- a/docs_src/tutorial/offset_and_limit/tutorial004.py +++ b/docs_src/tutorial/offset_and_limit/tutorial004_py39.py @@ -43,7 +43,7 @@ def create_heroes(): def select_heroes(): with Session(engine) as session: - statement = select(Hero).where(Hero.age > 32).limit(3) + statement = select(Hero).where(Hero.age > 32).offset(1).limit(2) results = session.exec(statement) heroes = results.all() print(heroes) diff --git a/docs_src/tutorial/one/tutorial001_py310.py b/docs_src/tutorial/one/tutorial001_py310.py new file mode 100644 index 0000000000..bb1326deef --- /dev/null +++ b/docs_src/tutorial/one/tutorial001_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.age <= 35) + results = session.exec(statement) + hero = results.first() + print("Hero:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/one/tutorial001.py b/docs_src/tutorial/one/tutorial001_py39.py similarity index 100% rename from docs_src/tutorial/one/tutorial001.py rename to docs_src/tutorial/one/tutorial001_py39.py diff --git a/docs_src/tutorial/one/tutorial002_py310.py b/docs_src/tutorial/one/tutorial002_py310.py new file mode 100644 index 0000000000..b82fcfd845 --- /dev/null +++ b/docs_src/tutorial/one/tutorial002_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.age < 25) + results = session.exec(statement) + hero = results.first() + print("Hero:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/one/tutorial002.py b/docs_src/tutorial/one/tutorial002_py39.py similarity index 100% rename from docs_src/tutorial/one/tutorial002.py rename to docs_src/tutorial/one/tutorial002_py39.py diff --git a/docs_src/tutorial/one/tutorial003_py310.py b/docs_src/tutorial/one/tutorial003_py310.py new file mode 100644 index 0000000000..f674c8a686 --- /dev/null +++ b/docs_src/tutorial/one/tutorial003_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Deadpond") + results = session.exec(statement) + hero = results.one() + print("Hero:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/one/tutorial003.py b/docs_src/tutorial/one/tutorial003_py39.py similarity index 100% rename from docs_src/tutorial/one/tutorial003.py rename to docs_src/tutorial/one/tutorial003_py39.py diff --git a/docs_src/tutorial/one/tutorial004_py310.py b/docs_src/tutorial/one/tutorial004_py310.py new file mode 100644 index 0000000000..e55b55304f --- /dev/null +++ b/docs_src/tutorial/one/tutorial004_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.age <= 35) + results = session.exec(statement) + hero = results.one() + print("Hero:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/one/tutorial004.py b/docs_src/tutorial/one/tutorial004_py39.py similarity index 100% rename from docs_src/tutorial/one/tutorial004.py rename to docs_src/tutorial/one/tutorial004_py39.py diff --git a/docs_src/tutorial/one/tutorial005_py310.py b/docs_src/tutorial/one/tutorial005_py310.py new file mode 100644 index 0000000000..6c51d8fab2 --- /dev/null +++ b/docs_src/tutorial/one/tutorial005_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.age < 25) + results = session.exec(statement) + hero = results.one() + print("Hero:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/one/tutorial005.py b/docs_src/tutorial/one/tutorial005_py39.py similarity index 100% rename from docs_src/tutorial/one/tutorial005.py rename to docs_src/tutorial/one/tutorial005_py39.py diff --git a/docs_src/tutorial/one/tutorial006_py310.py b/docs_src/tutorial/one/tutorial006_py310.py new file mode 100644 index 0000000000..6f9b7371ab --- /dev/null +++ b/docs_src/tutorial/one/tutorial006_py310.py @@ -0,0 +1,55 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + hero = session.exec(select(Hero).where(Hero.name == "Deadpond")).one() + print("Hero:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/one/tutorial006.py b/docs_src/tutorial/one/tutorial006_py39.py similarity index 100% rename from docs_src/tutorial/one/tutorial006.py rename to docs_src/tutorial/one/tutorial006_py39.py diff --git a/docs_src/tutorial/one/tutorial007_py310.py b/docs_src/tutorial/one/tutorial007_py310.py new file mode 100644 index 0000000000..f065f5ac78 --- /dev/null +++ b/docs_src/tutorial/one/tutorial007_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.id == 1) + results = session.exec(statement) + hero = results.first() + print("Hero:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/one/tutorial007.py b/docs_src/tutorial/one/tutorial007_py39.py similarity index 100% rename from docs_src/tutorial/one/tutorial007.py rename to docs_src/tutorial/one/tutorial007_py39.py diff --git a/docs_src/tutorial/one/tutorial008_py310.py b/docs_src/tutorial/one/tutorial008_py310.py new file mode 100644 index 0000000000..af9169704b --- /dev/null +++ b/docs_src/tutorial/one/tutorial008_py310.py @@ -0,0 +1,55 @@ +from sqlmodel import Field, Session, SQLModel, create_engine + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + hero = session.get(Hero, 1) + print("Hero:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/one/tutorial008.py b/docs_src/tutorial/one/tutorial008_py39.py similarity index 100% rename from docs_src/tutorial/one/tutorial008.py rename to docs_src/tutorial/one/tutorial008_py39.py diff --git a/docs_src/tutorial/one/tutorial009_py310.py b/docs_src/tutorial/one/tutorial009_py310.py new file mode 100644 index 0000000000..57db99ea42 --- /dev/null +++ b/docs_src/tutorial/one/tutorial009_py310.py @@ -0,0 +1,55 @@ +from sqlmodel import Field, Session, SQLModel, create_engine + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + hero = session.get(Hero, 9001) + print("Hero:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/one/tutorial009.py b/docs_src/tutorial/one/tutorial009_py39.py similarity index 100% rename from docs_src/tutorial/one/tutorial009.py rename to docs_src/tutorial/one/tutorial009_py39.py diff --git a/docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py b/docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py new file mode 100644 index 0000000000..a6ee1cafba --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py @@ -0,0 +1,141 @@ +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship() + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id") + team: Team | None = Relationship() + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + hero_spider_boy = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_deadpond) + session.add(hero_rusty_man) + session.add(hero_spider_boy) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + session.refresh(hero_spider_boy) + + print("Created hero:", hero_deadpond) + print("Created hero:", hero_rusty_man) + print("Created hero:", hero_spider_boy) + + hero_spider_boy.team = team_preventers + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") + team_wakaland = Team( + name="Wakaland", + headquarters="Wakaland Capital City", + heroes=[hero_black_lion, hero_sure_e], + ) + session.add(team_wakaland) + session.commit() + session.refresh(team_wakaland) + print("Team Wakaland:", team_wakaland) + + hero_tarantula = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_dr_weird = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_cap = Hero( + name="Captain North America", secret_name="Esteban Rogelios", age=93 + ) + + team_preventers.heroes.append(hero_tarantula) + team_preventers.heroes.append(hero_dr_weird) + team_preventers.heroes.append(hero_cap) + session.add(team_preventers) + session.commit() + session.refresh(hero_tarantula) + session.refresh(hero_dr_weird) + session.refresh(hero_cap) + print("Preventers new hero:", hero_tarantula) + print("Preventers new hero:", hero_dr_weird) + print("Preventers new hero:", hero_cap) + + +def select_heroes(): + with Session(engine) as session: + statement = select(Team).where(Team.name == "Preventers") + result = session.exec(statement) + team_preventers = result.one() + + print("Preventers heroes:", team_preventers.heroes) + + +def update_heroes(): + with Session(engine) as session: + hero_spider_boy = session.exec( + select(Hero).where(Hero.name == "Spider-Boy") + ).one() + + preventers_team = session.exec( + select(Team).where(Team.name == "Preventers") + ).one() + + print("Hero Spider-Boy:", hero_spider_boy) + print("Preventers Team:", preventers_team) + print("Preventers Team Heroes:", preventers_team.heroes) + + hero_spider_boy.team = None + + print("Spider-Boy without team:", hero_spider_boy) + + print("Preventers Team Heroes again:", preventers_team.heroes) + + session.add(hero_spider_boy) + session.commit() + print("After committing") + + session.refresh(hero_spider_boy) + print("Spider-Boy after commit:", hero_spider_boy) + + print("Preventers Team Heroes after commit:", preventers_team.heroes) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + update_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py b/docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py39.py similarity index 97% rename from docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py rename to docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py39.py index fc4eb97934..6ec5e72a7c 100644 --- a/docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py +++ b/docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py39.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Optional from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select @@ -8,7 +8,7 @@ class Team(SQLModel, table=True): name: str = Field(index=True) headquarters: str - heroes: List["Hero"] = Relationship() + heroes: list["Hero"] = Relationship() class Hero(SQLModel, table=True): @@ -34,7 +34,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", secret_name="Dive Wilson", team=team_z_force diff --git a/docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py b/docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py new file mode 100644 index 0000000000..978ed99787 --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py @@ -0,0 +1,141 @@ +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team") + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id") + team: Team | None = Relationship(back_populates="heroes") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + hero_spider_boy = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_deadpond) + session.add(hero_rusty_man) + session.add(hero_spider_boy) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + session.refresh(hero_spider_boy) + + print("Created hero:", hero_deadpond) + print("Created hero:", hero_rusty_man) + print("Created hero:", hero_spider_boy) + + hero_spider_boy.team = team_preventers + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") + team_wakaland = Team( + name="Wakaland", + headquarters="Wakaland Capital City", + heroes=[hero_black_lion, hero_sure_e], + ) + session.add(team_wakaland) + session.commit() + session.refresh(team_wakaland) + print("Team Wakaland:", team_wakaland) + + hero_tarantula = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_dr_weird = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_cap = Hero( + name="Captain North America", secret_name="Esteban Rogelios", age=93 + ) + + team_preventers.heroes.append(hero_tarantula) + team_preventers.heroes.append(hero_dr_weird) + team_preventers.heroes.append(hero_cap) + session.add(team_preventers) + session.commit() + session.refresh(hero_tarantula) + session.refresh(hero_dr_weird) + session.refresh(hero_cap) + print("Preventers new hero:", hero_tarantula) + print("Preventers new hero:", hero_dr_weird) + print("Preventers new hero:", hero_cap) + + +def select_heroes(): + with Session(engine) as session: + statement = select(Team).where(Team.name == "Preventers") + result = session.exec(statement) + team_preventers = result.one() + + print("Preventers heroes:", team_preventers.heroes) + + +def update_heroes(): + with Session(engine) as session: + hero_spider_boy = session.exec( + select(Hero).where(Hero.name == "Spider-Boy") + ).one() + + preventers_team = session.exec( + select(Team).where(Team.name == "Preventers") + ).one() + + print("Hero Spider-Boy:", hero_spider_boy) + print("Preventers Team:", preventers_team) + print("Preventers Team Heroes:", preventers_team.heroes) + + hero_spider_boy.team = None + + print("Spider-Boy without team:", hero_spider_boy) + + print("Preventers Team Heroes again:", preventers_team.heroes) + + session.add(hero_spider_boy) + session.commit() + print("After committing") + + session.refresh(hero_spider_boy) + print("Spider-Boy after commit:", hero_spider_boy) + + print("Preventers Team Heroes after commit:", preventers_team.heroes) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + update_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py b/docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py39.py similarity index 97% rename from docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py rename to docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py39.py index a25df4e75d..d001ef03da 100644 --- a/docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py +++ b/docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py39.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Optional from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select @@ -8,7 +8,7 @@ class Team(SQLModel, table=True): name: str = Field(index=True) headquarters: str - heroes: List["Hero"] = Relationship(back_populates="team") + heroes: list["Hero"] = Relationship(back_populates="team") class Hero(SQLModel, table=True): @@ -34,7 +34,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", secret_name="Dive Wilson", team=team_z_force diff --git a/docs_src/tutorial/relationship_attributes/back_populates/tutorial003_py310.py b/docs_src/tutorial/relationship_attributes/back_populates/tutorial003_py310.py new file mode 100644 index 0000000000..aa850b3f69 --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/back_populates/tutorial003_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Relationship, SQLModel, create_engine + + +class Weapon(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + + hero: "Hero" = Relationship(back_populates="weapon") + + +class Power(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + + hero_id: int = Field(foreign_key="hero.id") + hero: "Hero" = Relationship(back_populates="powers") + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team") + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id") + team: Team | None = Relationship(back_populates="heroes") + + weapon_id: int | None = Field(default=None, foreign_key="weapon.id") + weapon: Weapon | None = Relationship(back_populates="hero") + + powers: list[Power] = Relationship(back_populates="hero") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def main(): + create_db_and_tables() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/back_populates/tutorial003.py b/docs_src/tutorial/relationship_attributes/back_populates/tutorial003_py39.py similarity index 85% rename from docs_src/tutorial/relationship_attributes/back_populates/tutorial003.py rename to docs_src/tutorial/relationship_attributes/back_populates/tutorial003_py39.py index c137f58f6a..fc0d08df52 100644 --- a/docs_src/tutorial/relationship_attributes/back_populates/tutorial003.py +++ b/docs_src/tutorial/relationship_attributes/back_populates/tutorial003_py39.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Optional from sqlmodel import Field, Relationship, SQLModel, create_engine @@ -23,7 +23,7 @@ class Team(SQLModel, table=True): name: str = Field(index=True) headquarters: str - heroes: List["Hero"] = Relationship(back_populates="team") + heroes: list["Hero"] = Relationship(back_populates="team") class Hero(SQLModel, table=True): @@ -36,9 +36,9 @@ class Hero(SQLModel, table=True): team: Optional[Team] = Relationship(back_populates="heroes") weapon_id: Optional[int] = Field(default=None, foreign_key="weapon.id") - weapon: Optional[Weapon] = Relationship(back_populates="owner") + weapon: Optional[Weapon] = Relationship(back_populates="hero") - powers: List[Power] = Relationship(back_populates="hero") + powers: list[Power] = Relationship(back_populates="hero") sqlite_file_name = "database.db" diff --git a/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/__init__.py b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py310.py b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py310.py new file mode 100644 index 0000000000..8757aaa774 --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py310.py @@ -0,0 +1,106 @@ +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team", cascade_delete=True) + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id", ondelete="CASCADE") + team: Team | None = Relationship(back_populates="heroes") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + hero_spider_boy = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_deadpond) + session.add(hero_rusty_man) + session.add(hero_spider_boy) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + session.refresh(hero_spider_boy) + + print("Created hero:", hero_deadpond) + print("Created hero:", hero_rusty_man) + print("Created hero:", hero_spider_boy) + + hero_spider_boy.team = team_preventers + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") + team_wakaland = Team( + name="Wakaland", + headquarters="Wakaland Capital City", + heroes=[hero_black_lion, hero_sure_e], + ) + session.add(team_wakaland) + session.commit() + session.refresh(team_wakaland) + print("Team Wakaland:", team_wakaland) + + +def delete_team(): + with Session(engine) as session: + statement = select(Team).where(Team.name == "Wakaland") + team = session.exec(statement).one() + session.delete(team) + session.commit() + print("Deleted team:", team) + + +def select_deleted_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Black Lion") + result = session.exec(statement) + hero = result.first() + print("Black Lion not found:", hero) + + statement = select(Hero).where(Hero.name == "Princess Sure-E") + result = session.exec(statement) + hero = result.first() + print("Princess Sure-E not found:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + delete_team() + select_deleted_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py39.py b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py39.py new file mode 100644 index 0000000000..201f07675d --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py39.py @@ -0,0 +1,110 @@ +from typing import Optional + +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select + + +class Team(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team", cascade_delete=True) + + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + team_id: Optional[int] = Field( + default=None, foreign_key="team.id", ondelete="CASCADE" + ) + team: Optional[Team] = Relationship(back_populates="heroes") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + hero_spider_boy = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_deadpond) + session.add(hero_rusty_man) + session.add(hero_spider_boy) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + session.refresh(hero_spider_boy) + + print("Created hero:", hero_deadpond) + print("Created hero:", hero_rusty_man) + print("Created hero:", hero_spider_boy) + + hero_spider_boy.team = team_preventers + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") + team_wakaland = Team( + name="Wakaland", + headquarters="Wakaland Capital City", + heroes=[hero_black_lion, hero_sure_e], + ) + session.add(team_wakaland) + session.commit() + session.refresh(team_wakaland) + print("Team Wakaland:", team_wakaland) + + +def delete_team(): + with Session(engine) as session: + statement = select(Team).where(Team.name == "Wakaland") + team = session.exec(statement).one() + session.delete(team) + session.commit() + print("Deleted team:", team) + + +def select_deleted_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Black Lion") + result = session.exec(statement) + hero = result.first() + print("Black Lion not found:", hero) + + statement = select(Hero).where(Hero.name == "Princess Sure-E") + result = session.exec(statement) + hero = result.first() + print("Princess Sure-E not found:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + delete_team() + select_deleted_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002_py310.py b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002_py310.py new file mode 100644 index 0000000000..be42dba697 --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002_py310.py @@ -0,0 +1,108 @@ +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team") + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field( + default=None, foreign_key="team.id", ondelete="SET NULL" + ) + team: Team | None = Relationship(back_populates="heroes") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + hero_spider_boy = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_deadpond) + session.add(hero_rusty_man) + session.add(hero_spider_boy) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + session.refresh(hero_spider_boy) + + print("Created hero:", hero_deadpond) + print("Created hero:", hero_rusty_man) + print("Created hero:", hero_spider_boy) + + hero_spider_boy.team = team_preventers + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") + team_wakaland = Team( + name="Wakaland", + headquarters="Wakaland Capital City", + heroes=[hero_black_lion, hero_sure_e], + ) + session.add(team_wakaland) + session.commit() + session.refresh(team_wakaland) + print("Team Wakaland:", team_wakaland) + + +def delete_team(): + with Session(engine) as session: + statement = select(Team).where(Team.name == "Wakaland") + team = session.exec(statement).one() + session.delete(team) + session.commit() + print("Deleted team:", team) + + +def select_deleted_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Black Lion") + result = session.exec(statement) + hero = result.first() + print("Black Lion has no team:", hero) + + statement = select(Hero).where(Hero.name == "Princess Sure-E") + result = session.exec(statement) + hero = result.first() + print("Princess Sure-E has no team:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + delete_team() + select_deleted_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002_py39.py b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002_py39.py new file mode 100644 index 0000000000..7a22f6ee16 --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002_py39.py @@ -0,0 +1,110 @@ +from typing import Optional + +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select + + +class Team(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team") + + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + team_id: Optional[int] = Field( + default=None, foreign_key="team.id", ondelete="SET NULL" + ) + team: Optional[Team] = Relationship(back_populates="heroes") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + hero_spider_boy = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_deadpond) + session.add(hero_rusty_man) + session.add(hero_spider_boy) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + session.refresh(hero_spider_boy) + + print("Created hero:", hero_deadpond) + print("Created hero:", hero_rusty_man) + print("Created hero:", hero_spider_boy) + + hero_spider_boy.team = team_preventers + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") + team_wakaland = Team( + name="Wakaland", + headquarters="Wakaland Capital City", + heroes=[hero_black_lion, hero_sure_e], + ) + session.add(team_wakaland) + session.commit() + session.refresh(team_wakaland) + print("Team Wakaland:", team_wakaland) + + +def delete_team(): + with Session(engine) as session: + statement = select(Team).where(Team.name == "Wakaland") + team = session.exec(statement).one() + session.delete(team) + session.commit() + print("Deleted team:", team) + + +def select_deleted_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Black Lion") + result = session.exec(statement) + hero = result.first() + print("Black Lion has no team:", hero) + + statement = select(Hero).where(Hero.name == "Princess Sure-E") + result = session.exec(statement) + hero = result.first() + print("Princess Sure-E has no team:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + delete_team() + select_deleted_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003_py310.py b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003_py310.py new file mode 100644 index 0000000000..4623da1ed0 --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003_py310.py @@ -0,0 +1,110 @@ +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select, text + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team", passive_deletes="all") + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field( + default=None, foreign_key="team.id", ondelete="SET NULL" + ) + team: Team | None = Relationship(back_populates="heroes") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + with engine.connect() as connection: + connection.execute(text("PRAGMA foreign_keys=ON")) # for SQLite only + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + hero_spider_boy = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_deadpond) + session.add(hero_rusty_man) + session.add(hero_spider_boy) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + session.refresh(hero_spider_boy) + + print("Created hero:", hero_deadpond) + print("Created hero:", hero_rusty_man) + print("Created hero:", hero_spider_boy) + + hero_spider_boy.team = team_preventers + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") + team_wakaland = Team( + name="Wakaland", + headquarters="Wakaland Capital City", + heroes=[hero_black_lion, hero_sure_e], + ) + session.add(team_wakaland) + session.commit() + session.refresh(team_wakaland) + print("Team Wakaland:", team_wakaland) + + +def delete_team(): + with Session(engine) as session: + statement = select(Team).where(Team.name == "Wakaland") + team = session.exec(statement).one() + session.delete(team) + session.commit() + print("Deleted team:", team) + + +def select_deleted_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Black Lion") + result = session.exec(statement) + hero = result.first() + print("Black Lion has no team:", hero) + + statement = select(Hero).where(Hero.name == "Princess Sure-E") + result = session.exec(statement) + hero = result.first() + print("Princess Sure-E has no team:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + delete_team() + select_deleted_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003_py39.py b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003_py39.py new file mode 100644 index 0000000000..0327e69f50 --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003_py39.py @@ -0,0 +1,112 @@ +from typing import Optional + +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select, text + + +class Team(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team", passive_deletes="all") + + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + team_id: Optional[int] = Field( + default=None, foreign_key="team.id", ondelete="SET NULL" + ) + team: Optional[Team] = Relationship(back_populates="heroes") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + with engine.connect() as connection: + connection.execute(text("PRAGMA foreign_keys=ON")) # for SQLite only + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + hero_spider_boy = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_deadpond) + session.add(hero_rusty_man) + session.add(hero_spider_boy) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + session.refresh(hero_spider_boy) + + print("Created hero:", hero_deadpond) + print("Created hero:", hero_rusty_man) + print("Created hero:", hero_spider_boy) + + hero_spider_boy.team = team_preventers + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") + team_wakaland = Team( + name="Wakaland", + headquarters="Wakaland Capital City", + heroes=[hero_black_lion, hero_sure_e], + ) + session.add(team_wakaland) + session.commit() + session.refresh(team_wakaland) + print("Team Wakaland:", team_wakaland) + + +def delete_team(): + with Session(engine) as session: + statement = select(Team).where(Team.name == "Wakaland") + team = session.exec(statement).one() + session.delete(team) + session.commit() + print("Deleted team:", team) + + +def select_deleted_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Black Lion") + result = session.exec(statement) + hero = result.first() + print("Black Lion has no team:", hero) + + statement = select(Hero).where(Hero.name == "Princess Sure-E") + result = session.exec(statement) + hero = result.first() + print("Princess Sure-E has no team:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + delete_team() + select_deleted_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004_py310.py b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004_py310.py new file mode 100644 index 0000000000..051c14db34 --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004_py310.py @@ -0,0 +1,109 @@ +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select, text + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team", passive_deletes="all") + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field( + default=None, foreign_key="team.id", ondelete="RESTRICT" + ) + team: Team | None = Relationship(back_populates="heroes") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + with engine.connect() as connection: + connection.execute(text("PRAGMA foreign_keys=ON")) # for SQLite only + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + hero_spider_boy = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_deadpond) + session.add(hero_rusty_man) + session.add(hero_spider_boy) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + session.refresh(hero_spider_boy) + + print("Created hero:", hero_deadpond) + print("Created hero:", hero_rusty_man) + print("Created hero:", hero_spider_boy) + + hero_spider_boy.team = team_preventers + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") + team_wakaland = Team( + name="Wakaland", + headquarters="Wakaland Capital City", + heroes=[hero_black_lion, hero_sure_e], + ) + session.add(team_wakaland) + session.commit() + session.refresh(team_wakaland) + print("Team Wakaland:", team_wakaland) + + +def delete_team(): + with Session(engine) as session: + statement = select(Team).where(Team.name == "Wakaland") + team = session.exec(statement).one() + session.delete(team) + session.commit() + print("Deleted team:", team) + + +def select_deleted_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Black Lion") + result = session.exec(statement) + hero = result.first() + print("Black Lion has no team:", hero) + + statement = select(Hero).where(Hero.name == "Princess Sure-E") + result = session.exec(statement) + hero = result.first() + print("Princess Sure-E has no team:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + delete_team() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004_py39.py b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004_py39.py new file mode 100644 index 0000000000..25badb1fdf --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004_py39.py @@ -0,0 +1,111 @@ +from typing import Optional + +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select, text + + +class Team(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team", passive_deletes="all") + + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + team_id: Optional[int] = Field( + default=None, foreign_key="team.id", ondelete="RESTRICT" + ) + team: Optional[Team] = Relationship(back_populates="heroes") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + with engine.connect() as connection: + connection.execute(text("PRAGMA foreign_keys=ON")) # for SQLite only + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + hero_spider_boy = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_deadpond) + session.add(hero_rusty_man) + session.add(hero_spider_boy) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + session.refresh(hero_spider_boy) + + print("Created hero:", hero_deadpond) + print("Created hero:", hero_rusty_man) + print("Created hero:", hero_spider_boy) + + hero_spider_boy.team = team_preventers + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") + team_wakaland = Team( + name="Wakaland", + headquarters="Wakaland Capital City", + heroes=[hero_black_lion, hero_sure_e], + ) + session.add(team_wakaland) + session.commit() + session.refresh(team_wakaland) + print("Team Wakaland:", team_wakaland) + + +def delete_team(): + with Session(engine) as session: + statement = select(Team).where(Team.name == "Wakaland") + team = session.exec(statement).one() + session.delete(team) + session.commit() + print("Deleted team:", team) + + +def select_deleted_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Black Lion") + result = session.exec(statement) + hero = result.first() + print("Black Lion has no team:", hero) + + statement = select(Hero).where(Hero.name == "Princess Sure-E") + result = session.exec(statement) + hero = result.first() + print("Princess Sure-E has no team:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + delete_team() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial005_py310.py b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial005_py310.py new file mode 100644 index 0000000000..1d89bcae07 --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial005_py310.py @@ -0,0 +1,122 @@ +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select, text + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team", passive_deletes="all") + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field( + default=None, foreign_key="team.id", ondelete="RESTRICT" + ) + team: Team | None = Relationship(back_populates="heroes") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + with engine.connect() as connection: + connection.execute(text("PRAGMA foreign_keys=ON")) # for SQLite only + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + hero_spider_boy = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_deadpond) + session.add(hero_rusty_man) + session.add(hero_spider_boy) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + session.refresh(hero_spider_boy) + + print("Created hero:", hero_deadpond) + print("Created hero:", hero_rusty_man) + print("Created hero:", hero_spider_boy) + + hero_spider_boy.team = team_preventers + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") + team_wakaland = Team( + name="Wakaland", + headquarters="Wakaland Capital City", + heroes=[hero_black_lion, hero_sure_e], + ) + session.add(team_wakaland) + session.commit() + session.refresh(team_wakaland) + print("Team Wakaland:", team_wakaland) + + +def remove_team_heroes(): + with Session(engine) as session: + statement = select(Team).where(Team.name == "Wakaland") + team = session.exec(statement).one() + team.heroes.clear() + session.add(team) + session.commit() + session.refresh(team) + print("Team with removed heroes:", team) + + +def delete_team(): + with Session(engine) as session: + statement = select(Team).where(Team.name == "Wakaland") + team = session.exec(statement).one() + session.delete(team) + session.commit() + print("Deleted team:", team) + + +def select_deleted_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Black Lion") + result = session.exec(statement) + hero = result.first() + print("Black Lion has no team:", hero) + + statement = select(Hero).where(Hero.name == "Princess Sure-E") + result = session.exec(statement) + hero = result.first() + print("Princess Sure-E has no team:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + remove_team_heroes() + delete_team() + select_deleted_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial005_py39.py b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial005_py39.py new file mode 100644 index 0000000000..edc8fb0bd5 --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial005_py39.py @@ -0,0 +1,124 @@ +from typing import Optional + +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select, text + + +class Team(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team", passive_deletes="all") + + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + team_id: Optional[int] = Field( + default=None, foreign_key="team.id", ondelete="RESTRICT" + ) + team: Optional[Team] = Relationship(back_populates="heroes") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + with engine.connect() as connection: + connection.execute(text("PRAGMA foreign_keys=ON")) # for SQLite only + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + hero_spider_boy = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_deadpond) + session.add(hero_rusty_man) + session.add(hero_spider_boy) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + session.refresh(hero_spider_boy) + + print("Created hero:", hero_deadpond) + print("Created hero:", hero_rusty_man) + print("Created hero:", hero_spider_boy) + + hero_spider_boy.team = team_preventers + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") + team_wakaland = Team( + name="Wakaland", + headquarters="Wakaland Capital City", + heroes=[hero_black_lion, hero_sure_e], + ) + session.add(team_wakaland) + session.commit() + session.refresh(team_wakaland) + print("Team Wakaland:", team_wakaland) + + +def remove_team_heroes(): + with Session(engine) as session: + statement = select(Team).where(Team.name == "Wakaland") + team = session.exec(statement).one() + team.heroes.clear() + session.add(team) + session.commit() + session.refresh(team) + print("Team with removed heroes:", team) + + +def delete_team(): + with Session(engine) as session: + statement = select(Team).where(Team.name == "Wakaland") + team = session.exec(statement).one() + session.delete(team) + session.commit() + print("Deleted team:", team) + + +def select_deleted_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Black Lion") + result = session.exec(statement) + hero = result.first() + print("Black Lion has no team:", hero) + + statement = select(Hero).where(Hero.name == "Princess Sure-E") + result = session.exec(statement) + hero = result.first() + print("Princess Sure-E has no team:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + remove_team_heroes() + delete_team() + select_deleted_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py310.py b/docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py310.py new file mode 100644 index 0000000000..35567360e1 --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py310.py @@ -0,0 +1,100 @@ +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team") + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id") + team: Team | None = Relationship(back_populates="heroes") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + hero_spider_boy = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_deadpond) + session.add(hero_rusty_man) + session.add(hero_spider_boy) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + session.refresh(hero_spider_boy) + + print("Created hero:", hero_deadpond) + print("Created hero:", hero_rusty_man) + print("Created hero:", hero_spider_boy) + + hero_spider_boy.team = team_preventers + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") + team_wakaland = Team( + name="Wakaland", + headquarters="Wakaland Capital City", + heroes=[hero_black_lion, hero_sure_e], + ) + session.add(team_wakaland) + session.commit() + session.refresh(team_wakaland) + print("Team Wakaland:", team_wakaland) + + hero_tarantula = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_dr_weird = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_cap = Hero( + name="Captain North America", secret_name="Esteban Rogelios", age=93 + ) + + team_preventers.heroes.append(hero_tarantula) + team_preventers.heroes.append(hero_dr_weird) + team_preventers.heroes.append(hero_cap) + session.add(team_preventers) + session.commit() + session.refresh(hero_tarantula) + session.refresh(hero_dr_weird) + session.refresh(hero_cap) + print("Preventers new hero:", hero_tarantula) + print("Preventers new hero:", hero_dr_weird) + print("Preventers new hero:", hero_cap) + + +def main(): + create_db_and_tables() + create_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py b/docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py39.py similarity index 96% rename from docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py rename to docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py39.py index ec9c909d73..43c699d3d9 100644 --- a/docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py +++ b/docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py39.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Optional from sqlmodel import Field, Relationship, Session, SQLModel, create_engine @@ -8,7 +8,7 @@ class Team(SQLModel, table=True): name: str = Field(index=True) headquarters: str - heroes: List["Hero"] = Relationship(back_populates="team") + heroes: list["Hero"] = Relationship(back_populates="team") class Hero(SQLModel, table=True): @@ -34,7 +34,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", secret_name="Dive Wilson", team=team_z_force diff --git a/docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py310.py b/docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py310.py new file mode 100644 index 0000000000..5df80e9f86 --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py310.py @@ -0,0 +1,68 @@ +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team") + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id") + team: Team | None = Relationship(back_populates="heroes") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + hero_spider_boy = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_deadpond) + session.add(hero_rusty_man) + session.add(hero_spider_boy) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + session.refresh(hero_spider_boy) + + print("Created hero:", hero_deadpond) + print("Created hero:", hero_rusty_man) + print("Created hero:", hero_spider_boy) + + hero_spider_boy.team = team_preventers + session.add(hero_spider_boy) + session.commit() + + +def main(): + create_db_and_tables() + create_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py b/docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py39.py similarity index 94% rename from docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py rename to docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py39.py index 71cb3f6136..8530f67b39 100644 --- a/docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py +++ b/docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py39.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Optional from sqlmodel import Field, Relationship, Session, SQLModel, create_engine @@ -8,7 +8,7 @@ class Team(SQLModel, table=True): name: str = Field(index=True) headquarters: str - heroes: List["Hero"] = Relationship(back_populates="team") + heroes: list["Hero"] = Relationship(back_populates="team") class Hero(SQLModel, table=True): @@ -34,7 +34,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", secret_name="Dive Wilson", team=team_z_force diff --git a/docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py310.py b/docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py310.py new file mode 100644 index 0000000000..abae89d2eb --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py310.py @@ -0,0 +1,115 @@ +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team") + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id") + team: Team | None = Relationship(back_populates="heroes") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + hero_spider_boy = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_deadpond) + session.add(hero_rusty_man) + session.add(hero_spider_boy) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + session.refresh(hero_spider_boy) + + print("Created hero:", hero_deadpond) + print("Created hero:", hero_rusty_man) + print("Created hero:", hero_spider_boy) + + hero_spider_boy.team = team_preventers + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") + team_wakaland = Team( + name="Wakaland", + headquarters="Wakaland Capital City", + heroes=[hero_black_lion, hero_sure_e], + ) + session.add(team_wakaland) + session.commit() + session.refresh(team_wakaland) + print("Team Wakaland:", team_wakaland) + + hero_tarantula = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_dr_weird = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_cap = Hero( + name="Captain North America", secret_name="Esteban Rogelios", age=93 + ) + + team_preventers.heroes.append(hero_tarantula) + team_preventers.heroes.append(hero_dr_weird) + team_preventers.heroes.append(hero_cap) + session.add(team_preventers) + session.commit() + session.refresh(hero_tarantula) + session.refresh(hero_dr_weird) + session.refresh(hero_cap) + print("Preventers new hero:", hero_tarantula) + print("Preventers new hero:", hero_dr_weird) + print("Preventers new hero:", hero_cap) + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Spider-Boy") + result = session.exec(statement) + hero_spider_boy = result.one() + + statement = select(Team).where(Team.id == hero_spider_boy.team_id) + result = session.exec(statement) + team = result.first() + print("Spider-Boy's team:", team) + + print("Spider-Boy's team again:", hero_spider_boy.team) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py b/docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py39.py similarity index 96% rename from docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py rename to docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py39.py index 5f718cab45..2219c82dab 100644 --- a/docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py +++ b/docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py39.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Optional from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select @@ -8,7 +8,7 @@ class Team(SQLModel, table=True): name: str = Field(index=True) headquarters: str - heroes: List["Hero"] = Relationship(back_populates="team") + heroes: list["Hero"] = Relationship(back_populates="team") class Hero(SQLModel, table=True): @@ -34,7 +34,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", secret_name="Dive Wilson", team=team_z_force @@ -99,7 +99,7 @@ def select_heroes(): result = session.exec(statement) hero_spider_boy = result.one() - statement = select(Team).where(Team.id == hero_spider_boy.id) + statement = select(Team).where(Team.id == hero_spider_boy.team_id) result = session.exec(statement) team = result.first() print("Spider-Boy's team:", team) diff --git a/docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py310.py b/docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py310.py new file mode 100644 index 0000000000..0221d8834c --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py310.py @@ -0,0 +1,125 @@ +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team") + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id") + team: Team | None = Relationship(back_populates="heroes") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + hero_spider_boy = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_deadpond) + session.add(hero_rusty_man) + session.add(hero_spider_boy) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + session.refresh(hero_spider_boy) + + print("Created hero:", hero_deadpond) + print("Created hero:", hero_rusty_man) + print("Created hero:", hero_spider_boy) + + hero_spider_boy.team = team_preventers + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") + team_wakaland = Team( + name="Wakaland", + headquarters="Wakaland Capital City", + heroes=[hero_black_lion, hero_sure_e], + ) + session.add(team_wakaland) + session.commit() + session.refresh(team_wakaland) + print("Team Wakaland:", team_wakaland) + + hero_tarantula = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_dr_weird = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_cap = Hero( + name="Captain North America", secret_name="Esteban Rogelios", age=93 + ) + + team_preventers.heroes.append(hero_tarantula) + team_preventers.heroes.append(hero_dr_weird) + team_preventers.heroes.append(hero_cap) + session.add(team_preventers) + session.commit() + session.refresh(hero_tarantula) + session.refresh(hero_dr_weird) + session.refresh(hero_cap) + print("Preventers new hero:", hero_tarantula) + print("Preventers new hero:", hero_dr_weird) + print("Preventers new hero:", hero_cap) + + +def select_heroes(): + with Session(engine) as session: + statement = select(Team).where(Team.name == "Preventers") + result = session.exec(statement) + team_preventers = result.one() + + print("Preventers heroes:", team_preventers.heroes) + + +def update_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Spider-Boy") + result = session.exec(statement) + hero_spider_boy = result.one() + + hero_spider_boy.team = None + session.add(hero_spider_boy) + session.commit() + + session.refresh(hero_spider_boy) + print("Spider-Boy without team:", hero_spider_boy) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + update_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/read_relationships/tutorial002.py b/docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py39.py similarity index 97% rename from docs_src/tutorial/relationship_attributes/read_relationships/tutorial002.py rename to docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py39.py index fdb436eb5f..393bdc8a5e 100644 --- a/docs_src/tutorial/relationship_attributes/read_relationships/tutorial002.py +++ b/docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py39.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Optional from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select @@ -8,7 +8,7 @@ class Team(SQLModel, table=True): name: str = Field(index=True) headquarters: str - heroes: List["Hero"] = Relationship(back_populates="team") + heroes: list["Hero"] = Relationship(back_populates="team") class Hero(SQLModel, table=True): @@ -34,7 +34,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", secret_name="Dive Wilson", team=team_z_force diff --git a/docs_src/tutorial/select/annotations/en/tutorial002.md b/docs_src/tutorial/select/annotations/en/tutorial002.md index 2570b542c4..fa7e6f3a55 100644 --- a/docs_src/tutorial/select/annotations/en/tutorial002.md +++ b/docs_src/tutorial/select/annotations/en/tutorial002.md @@ -16,10 +16,13 @@ 7. Create a new **session** to query data. - !!! tip - Notice that this is a new **session** independent from the one in the other function above. + /// tip - But it still uses the same **engine**. We still have one engine for the whole application. + Notice that this is a new **session** independent from the one in the other function above. + + But it still uses the same **engine**. We still have one engine for the whole application. + + /// 8. Use the `select()` function to create a statement selecting all the `Hero` objects. @@ -35,7 +38,7 @@ ``` INFO Engine BEGIN (implicit) - INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age + INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age FROM hero INFO Engine [no key 0.00032s] () ``` diff --git a/docs_src/tutorial/select/tutorial001_py310.py b/docs_src/tutorial/select/tutorial001_py310.py new file mode 100644 index 0000000000..29bce28251 --- /dev/null +++ b/docs_src/tutorial/select/tutorial001_py310.py @@ -0,0 +1,49 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero) + results = session.exec(statement) + for hero in results: + print(hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/select/tutorial001.py b/docs_src/tutorial/select/tutorial001_py39.py similarity index 100% rename from docs_src/tutorial/select/tutorial001.py rename to docs_src/tutorial/select/tutorial001_py39.py diff --git a/docs_src/tutorial/select/tutorial002_py310.py b/docs_src/tutorial/select/tutorial002_py310.py new file mode 100644 index 0000000000..b2f9d4d22c --- /dev/null +++ b/docs_src/tutorial/select/tutorial002_py310.py @@ -0,0 +1,50 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select # (1)! + + +class Hero(SQLModel, table=True): # (2)! + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) # (3)! + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) # (4)! + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") # (5)! + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + + with Session(engine) as session: # (6)! + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: # (7)! + statement = select(Hero) # (8)! + results = session.exec(statement) # (9)! + for hero in results: # (10)! + print(hero) # (11)! + # (12)! + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() # (13)! + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/select/tutorial002.py b/docs_src/tutorial/select/tutorial002_py39.py similarity index 63% rename from docs_src/tutorial/select/tutorial002.py rename to docs_src/tutorial/select/tutorial002_py39.py index 4f6d102827..2229f3fc30 100644 --- a/docs_src/tutorial/select/tutorial002.py +++ b/docs_src/tutorial/select/tutorial002_py39.py @@ -1,9 +1,9 @@ from typing import Optional -from sqlmodel import Field, Session, SQLModel, create_engine, select # (1) +from sqlmodel import Field, Session, SQLModel, create_engine, select # (1)! -class Hero(SQLModel, table=True): # (2) +class Hero(SQLModel, table=True): # (2)! id: Optional[int] = Field(default=None, primary_key=True) name: str secret_name: str @@ -13,19 +13,19 @@ class Hero(SQLModel, table=True): # (2) sqlite_file_name = "database.db" sqlite_url = f"sqlite:///{sqlite_file_name}" -engine = create_engine(sqlite_url, echo=True) # (3) +engine = create_engine(sqlite_url, echo=True) # (3)! def create_db_and_tables(): - SQLModel.metadata.create_all(engine) # (4) + SQLModel.metadata.create_all(engine) # (4)! def create_heroes(): - hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") # (5) + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") # (5)! hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) - with Session(engine) as session: # (6) + with Session(engine) as session: # (6)! session.add(hero_1) session.add(hero_2) session.add(hero_3) @@ -34,18 +34,18 @@ def create_heroes(): def select_heroes(): - with Session(engine) as session: # (7) - statement = select(Hero) # (8) - results = session.exec(statement) # (9) - for hero in results: # (10) - print(hero) # (11) - # (12) + with Session(engine) as session: # (7)! + statement = select(Hero) # (8)! + results = session.exec(statement) # (9)! + for hero in results: # (10)! + print(hero) # (11)! + # (12)! def main(): create_db_and_tables() create_heroes() - select_heroes() # (13) + select_heroes() # (13)! if __name__ == "__main__": diff --git a/docs_src/tutorial/select/tutorial003_py310.py b/docs_src/tutorial/select/tutorial003_py310.py new file mode 100644 index 0000000000..836998e24e --- /dev/null +++ b/docs_src/tutorial/select/tutorial003_py310.py @@ -0,0 +1,49 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero) + results = session.exec(statement) + heroes = results.all() + print(heroes) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/select/tutorial003.py b/docs_src/tutorial/select/tutorial003_py39.py similarity index 100% rename from docs_src/tutorial/select/tutorial003.py rename to docs_src/tutorial/select/tutorial003_py39.py diff --git a/docs_src/tutorial/select/tutorial004_py310.py b/docs_src/tutorial/select/tutorial004_py310.py new file mode 100644 index 0000000000..6366d40865 --- /dev/null +++ b/docs_src/tutorial/select/tutorial004_py310.py @@ -0,0 +1,47 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + heroes = session.exec(select(Hero)).all() + print(heroes) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/select/tutorial004.py b/docs_src/tutorial/select/tutorial004_py39.py similarity index 100% rename from docs_src/tutorial/select/tutorial004.py rename to docs_src/tutorial/select/tutorial004_py39.py diff --git a/docs_src/tutorial/update/annotations/en/tutorial002.md b/docs_src/tutorial/update/annotations/en/tutorial002.md index 3a52bd9bdf..7cb7c28ba8 100644 --- a/docs_src/tutorial/update/annotations/en/tutorial002.md +++ b/docs_src/tutorial/update/annotations/en/tutorial002.md @@ -5,18 +5,21 @@ This generates the output: ``` - INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age - FROM hero + INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age + FROM hero WHERE hero.name = ? INFO Engine [no key 0.00017s] ('Spider-Boy',) ``` 3. Get one hero object, expecting exactly one. - !!! tip - This ensures there's no more than one, and that there's exactly one, not `None`. + /// tip - This would never return `None`, instead it would raise an exception. + This ensures there's no more than one, and that there's exactly one, not `None`. + + This would never return `None`, instead it would raise an exception. + + /// 4. Print the hero object. @@ -53,8 +56,8 @@ This generates the output: ``` - INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age - FROM hero + INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age + FROM hero WHERE hero.id = ? INFO Engine [generated in 0.00018s] (2,) ``` diff --git a/docs_src/tutorial/update/annotations/en/tutorial004.md b/docs_src/tutorial/update/annotations/en/tutorial004.md index 55755cd88d..bbb713ead5 100644 --- a/docs_src/tutorial/update/annotations/en/tutorial004.md +++ b/docs_src/tutorial/update/annotations/en/tutorial004.md @@ -5,8 +5,8 @@ This generates the output: ``` - INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age - FROM hero + INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age + FROM hero WHERE hero.name = ? INFO Engine [no key 0.00018s] ('Spider-Boy',) ``` @@ -29,18 +29,21 @@ ``` INFO Engine BEGIN (implicit) - INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age - FROM hero + INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age + FROM hero WHERE hero.name = ? INFO Engine [no key 0.00020s] ('Captain North America',) ``` - !!! tip - See the `BEGIN` at the top? + /// tip - This is SQLAlchemy automatically starting a transaction for us. + See the `BEGIN` at the top? - This way, we could revert the last changes (if there were some) if we wanted to, even if the SQL to create them was already sent to the database. + This is SQLAlchemy automatically starting a transaction for us. + + This way, we could revert the last changes (if there were some) if we wanted to, even if the SQL to create them was already sent to the database. + + /// 7. Get one hero object for this new query. @@ -98,10 +101,13 @@ INFO Engine COMMIT ``` - !!! tip - See how SQLAlchemy (that powers SQLModel) optimizes the SQL to do as much work as possible in a single batch. + /// tip + + See how SQLAlchemy (that powers SQLModel) optimizes the SQL to do as much work as possible in a single batch. + + Here it updates both heroes in a single SQL query. - Here it updates both heroes in a single SQL query. + /// 16. Refresh the first hero. @@ -109,30 +115,36 @@ ``` INFO Engine BEGIN (implicit) - INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age - FROM hero + INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age + FROM hero WHERE hero.id = ? INFO Engine [generated in 0.00023s] (2,) ``` - !!! tip - Because we just committed a SQL transaction with `COMMIT`, SQLAlchemy will automatically start a new transaction with `BEGIN`. + /// tip + + Because we just committed a SQL transaction with `COMMIT`, SQLAlchemy will automatically start a new transaction with `BEGIN`. + + /// 17. Refresh the second hero. This generates the output: ``` - INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age - FROM hero + INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age + FROM hero WHERE hero.id = ? INFO Engine [cached since 0.001709s ago] (7,) ``` - !!! tip - SQLAlchemy is still using the previous transaction, so it doesn't have to create a new one. + /// tip + + SQLAlchemy is still using the previous transaction, so it doesn't have to create a new one. + + /// -18. Print the first hero, now udpated. +18. Print the first hero, now updated. This generates the output: diff --git a/docs_src/tutorial/update/tutorial001_py310.py b/docs_src/tutorial/update/tutorial001_py310.py new file mode 100644 index 0000000000..6cc4f48934 --- /dev/null +++ b/docs_src/tutorial/update/tutorial001_py310.py @@ -0,0 +1,63 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def update_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Spider-Boy") + results = session.exec(statement) + hero = results.one() + print("Hero:", hero) + + hero.age = 16 + session.add(hero) + session.commit() + session.refresh(hero) + print("Updated hero:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + update_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/update/tutorial001.py b/docs_src/tutorial/update/tutorial001_py39.py similarity index 100% rename from docs_src/tutorial/update/tutorial001.py rename to docs_src/tutorial/update/tutorial001_py39.py diff --git a/docs_src/tutorial/update/tutorial002_py310.py b/docs_src/tutorial/update/tutorial002_py310.py new file mode 100644 index 0000000000..64cb6916d8 --- /dev/null +++ b/docs_src/tutorial/update/tutorial002_py310.py @@ -0,0 +1,63 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def update_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Spider-Boy") # (1)! + results = session.exec(statement) # (2)! + hero = results.one() # (3)! + print("Hero:", hero) # (4)! + + hero.age = 16 # (5)! + session.add(hero) # (6)! + session.commit() # (7)! + session.refresh(hero) # (8)! + print("Updated hero:", hero) # (9)! + + +def main(): + create_db_and_tables() + create_heroes() + update_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/update/tutorial002.py b/docs_src/tutorial/update/tutorial002_py39.py similarity index 83% rename from docs_src/tutorial/update/tutorial002.py rename to docs_src/tutorial/update/tutorial002_py39.py index 4880f73f90..572198563c 100644 --- a/docs_src/tutorial/update/tutorial002.py +++ b/docs_src/tutorial/update/tutorial002_py39.py @@ -43,16 +43,16 @@ def create_heroes(): def update_heroes(): with Session(engine) as session: - statement = select(Hero).where(Hero.name == "Spider-Boy") # (1) - results = session.exec(statement) # (2) - hero = results.one() # (3) - print("Hero:", hero) # (4) - - hero.age = 16 # (5) - session.add(hero) # (6) - session.commit() # (7) - session.refresh(hero) # (8) - print("Updated hero:", hero) # (9) + statement = select(Hero).where(Hero.name == "Spider-Boy") # (1)! + results = session.exec(statement) # (2)! + hero = results.one() # (3)! + print("Hero:", hero) # (4)! + + hero.age = 16 # (5)! + session.add(hero) # (6)! + session.commit() # (7)! + session.refresh(hero) # (8)! + print("Updated hero:", hero) # (9)! def main(): diff --git a/docs_src/tutorial/update/tutorial003_py310.py b/docs_src/tutorial/update/tutorial003_py310.py new file mode 100644 index 0000000000..f250b071c1 --- /dev/null +++ b/docs_src/tutorial/update/tutorial003_py310.py @@ -0,0 +1,77 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def update_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Spider-Boy") + results = session.exec(statement) + hero_1 = results.one() + print("Hero 1:", hero_1) + + statement = select(Hero).where(Hero.name == "Captain North America") + results = session.exec(statement) + hero_2 = results.one() + print("Hero 2:", hero_2) + + hero_1.age = 16 + hero_1.name = "Spider-Youngster" + session.add(hero_1) + + hero_2.name = "Captain North America Except Canada" + hero_2.age = 110 + session.add(hero_2) + + session.commit() + session.refresh(hero_1) + session.refresh(hero_2) + + print("Updated hero 1:", hero_1) + print("Updated hero 2:", hero_2) + + +def main(): + create_db_and_tables() + create_heroes() + update_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/update/tutorial003.py b/docs_src/tutorial/update/tutorial003_py39.py similarity index 100% rename from docs_src/tutorial/update/tutorial003.py rename to docs_src/tutorial/update/tutorial003_py39.py diff --git a/docs_src/tutorial/update/tutorial004_py310.py b/docs_src/tutorial/update/tutorial004_py310.py new file mode 100644 index 0000000000..09e54e1cce --- /dev/null +++ b/docs_src/tutorial/update/tutorial004_py310.py @@ -0,0 +1,78 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def update_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Spider-Boy") # (1)! + results = session.exec(statement) # (2)! + hero_1 = results.one() # (3)! + print("Hero 1:", hero_1) # (4)! + + statement = select(Hero).where(Hero.name == "Captain North America") # (5)! + results = session.exec(statement) # (6)! + hero_2 = results.one() # (7)! + print("Hero 2:", hero_2) # (8)! + + hero_1.age = 16 # (9)! + hero_1.name = "Spider-Youngster" # (10)! + session.add(hero_1) # (11)! + + hero_2.name = "Captain North America Except Canada" # (12)! + hero_2.age = 110 # (13)! + session.add(hero_2) # (14)! + + session.commit() # (15)! + session.refresh(hero_1) # (16)! + session.refresh(hero_2) # (17)! + + print("Updated hero 1:", hero_1) # (18)! + print("Updated hero 2:", hero_2) # (19)! + # (20)! + + +def main(): + create_db_and_tables() + create_heroes() + update_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/update/tutorial004.py b/docs_src/tutorial/update/tutorial004_py39.py similarity index 70% rename from docs_src/tutorial/update/tutorial004.py rename to docs_src/tutorial/update/tutorial004_py39.py index 868c66c7d4..c74316e7b6 100644 --- a/docs_src/tutorial/update/tutorial004.py +++ b/docs_src/tutorial/update/tutorial004_py39.py @@ -43,31 +43,31 @@ def create_heroes(): def update_heroes(): with Session(engine) as session: - statement = select(Hero).where(Hero.name == "Spider-Boy") # (1) - results = session.exec(statement) # (2) - hero_1 = results.one() # (3) - print("Hero 1:", hero_1) # (4) - - statement = select(Hero).where(Hero.name == "Captain North America") # (5) - results = session.exec(statement) # (6) - hero_2 = results.one() # (7) - print("Hero 2:", hero_2) # (8) - - hero_1.age = 16 # (9) - hero_1.name = "Spider-Youngster" # (10) - session.add(hero_1) # (11) - - hero_2.name = "Captain North America Except Canada" # (12) - hero_2.age = 110 # (13) - session.add(hero_2) # (14) - - session.commit() # (15) - session.refresh(hero_1) # (16) - session.refresh(hero_2) # (17) - - print("Updated hero 1:", hero_1) # (18) - print("Updated hero 2:", hero_2) # (19) - # (20) + statement = select(Hero).where(Hero.name == "Spider-Boy") # (1)! + results = session.exec(statement) # (2)! + hero_1 = results.one() # (3)! + print("Hero 1:", hero_1) # (4)! + + statement = select(Hero).where(Hero.name == "Captain North America") # (5)! + results = session.exec(statement) # (6)! + hero_2 = results.one() # (7)! + print("Hero 2:", hero_2) # (8)! + + hero_1.age = 16 # (9)! + hero_1.name = "Spider-Youngster" # (10)! + session.add(hero_1) # (11)! + + hero_2.name = "Captain North America Except Canada" # (12)! + hero_2.age = 110 # (13)! + session.add(hero_2) # (14)! + + session.commit() # (15)! + session.refresh(hero_1) # (16)! + session.refresh(hero_2) # (17)! + + print("Updated hero 1:", hero_1) # (18)! + print("Updated hero 2:", hero_2) # (19)! + # (20)! def main(): diff --git a/docs_src/tutorial/where/tutorial001_py310.py b/docs_src/tutorial/where/tutorial001_py310.py new file mode 100644 index 0000000000..a59e5fc281 --- /dev/null +++ b/docs_src/tutorial/where/tutorial001_py310.py @@ -0,0 +1,49 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Deadpond") + results = session.exec(statement) + for hero in results: + print(hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/where/tutorial001.py b/docs_src/tutorial/where/tutorial001_py39.py similarity index 100% rename from docs_src/tutorial/where/tutorial001.py rename to docs_src/tutorial/where/tutorial001_py39.py diff --git a/docs_src/tutorial/where/tutorial002_py310.py b/docs_src/tutorial/where/tutorial002_py310.py new file mode 100644 index 0000000000..5db10c5808 --- /dev/null +++ b/docs_src/tutorial/where/tutorial002_py310.py @@ -0,0 +1,49 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name != "Deadpond") + results = session.exec(statement) + for hero in results: + print(hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/where/tutorial002.py b/docs_src/tutorial/where/tutorial002_py39.py similarity index 100% rename from docs_src/tutorial/where/tutorial002.py rename to docs_src/tutorial/where/tutorial002_py39.py diff --git a/docs_src/tutorial/where/tutorial003_py310.py b/docs_src/tutorial/where/tutorial003_py310.py new file mode 100644 index 0000000000..c368add499 --- /dev/null +++ b/docs_src/tutorial/where/tutorial003_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.age > 35) + results = session.exec(statement) + for hero in results: + print(hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/where/tutorial003.py b/docs_src/tutorial/where/tutorial003_py39.py similarity index 100% rename from docs_src/tutorial/where/tutorial003.py rename to docs_src/tutorial/where/tutorial003_py39.py diff --git a/docs_src/tutorial/where/tutorial004_py310.py b/docs_src/tutorial/where/tutorial004_py310.py new file mode 100644 index 0000000000..5733b71795 --- /dev/null +++ b/docs_src/tutorial/where/tutorial004_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.age >= 35) + results = session.exec(statement) + for hero in results: + print(hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/where/tutorial004.py b/docs_src/tutorial/where/tutorial004_py39.py similarity index 100% rename from docs_src/tutorial/where/tutorial004.py rename to docs_src/tutorial/where/tutorial004_py39.py diff --git a/docs_src/tutorial/where/tutorial005_py310.py b/docs_src/tutorial/where/tutorial005_py310.py new file mode 100644 index 0000000000..5251506970 --- /dev/null +++ b/docs_src/tutorial/where/tutorial005_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.age < 35) + results = session.exec(statement) + for hero in results: + print(hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/where/tutorial005.py b/docs_src/tutorial/where/tutorial005_py39.py similarity index 100% rename from docs_src/tutorial/where/tutorial005.py rename to docs_src/tutorial/where/tutorial005_py39.py diff --git a/docs_src/tutorial/where/tutorial006_py310.py b/docs_src/tutorial/where/tutorial006_py310.py new file mode 100644 index 0000000000..a3ab8507e3 --- /dev/null +++ b/docs_src/tutorial/where/tutorial006_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.age <= 35) + results = session.exec(statement) + for hero in results: + print(hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/where/tutorial006.py b/docs_src/tutorial/where/tutorial006_py39.py similarity index 100% rename from docs_src/tutorial/where/tutorial006.py rename to docs_src/tutorial/where/tutorial006_py39.py diff --git a/docs_src/tutorial/where/tutorial007_py310.py b/docs_src/tutorial/where/tutorial007_py310.py new file mode 100644 index 0000000000..589bc98671 --- /dev/null +++ b/docs_src/tutorial/where/tutorial007_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.age >= 35).where(Hero.age < 40) + results = session.exec(statement) + for hero in results: + print(hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/where/tutorial007.py b/docs_src/tutorial/where/tutorial007_py39.py similarity index 100% rename from docs_src/tutorial/where/tutorial007.py rename to docs_src/tutorial/where/tutorial007_py39.py diff --git a/docs_src/tutorial/where/tutorial008_py310.py b/docs_src/tutorial/where/tutorial008_py310.py new file mode 100644 index 0000000000..f32260c9f9 --- /dev/null +++ b/docs_src/tutorial/where/tutorial008_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.age >= 35, Hero.age < 40) + results = session.exec(statement) + for hero in results: + print(hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/where/tutorial008.py b/docs_src/tutorial/where/tutorial008_py39.py similarity index 100% rename from docs_src/tutorial/where/tutorial008.py rename to docs_src/tutorial/where/tutorial008_py39.py diff --git a/docs_src/tutorial/where/tutorial009_py310.py b/docs_src/tutorial/where/tutorial009_py310.py new file mode 100644 index 0000000000..0681d1c0a5 --- /dev/null +++ b/docs_src/tutorial/where/tutorial009_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, or_, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(or_(Hero.age <= 35, Hero.age > 90)) + results = session.exec(statement) + for hero in results: + print(hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/where/tutorial009.py b/docs_src/tutorial/where/tutorial009_py39.py similarity index 100% rename from docs_src/tutorial/where/tutorial009.py rename to docs_src/tutorial/where/tutorial009_py39.py diff --git a/docs_src/tutorial/where/tutorial010_py310.py b/docs_src/tutorial/where/tutorial010_py310.py new file mode 100644 index 0000000000..a65c47acf2 --- /dev/null +++ b/docs_src/tutorial/where/tutorial010_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where((Hero.age <= 35) | (Hero.age > 90)) + results = session.exec(statement) + for hero in results: + print(hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/where/tutorial010.py b/docs_src/tutorial/where/tutorial010_py39.py similarity index 100% rename from docs_src/tutorial/where/tutorial010.py rename to docs_src/tutorial/where/tutorial010_py39.py diff --git a/docs_src/tutorial/where/tutorial011_py310.py b/docs_src/tutorial/where/tutorial011_py310.py new file mode 100644 index 0000000000..73aa4aa8c0 --- /dev/null +++ b/docs_src/tutorial/where/tutorial011_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Session, SQLModel, col, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(col(Hero.age) >= 35) + results = session.exec(statement) + for hero in results: + print(hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/where/tutorial011.py b/docs_src/tutorial/where/tutorial011_py39.py similarity index 100% rename from docs_src/tutorial/where/tutorial011.py rename to docs_src/tutorial/where/tutorial011_py39.py diff --git a/mkdocs.env.yml b/mkdocs.env.yml new file mode 100644 index 0000000000..545d30a18c --- /dev/null +++ b/mkdocs.env.yml @@ -0,0 +1,5 @@ +# Define this here and not in the main mkdocs.yml file because that one could be auto +# updated and written, and the script would remove the env var +markdown_extensions: + pymdownx.highlight: + linenums: !ENV [LINENUMS, false] diff --git a/mkdocs.insiders.yml b/mkdocs.insiders.yml deleted file mode 100644 index 9f2775ff97..0000000000 --- a/mkdocs.insiders.yml +++ /dev/null @@ -1,4 +0,0 @@ -INHERIT: mkdocs.yml -plugins: - - search - - social diff --git a/mkdocs.yml b/mkdocs.yml index 41a7258a75..b89516e024 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,118 +1,201 @@ +INHERIT: ./mkdocs.env.yml site_name: SQLModel site_description: SQLModel, SQL databases in Python, designed for simplicity, compatibility, and robustness. site_url: https://sqlmodel.tiangolo.com/ theme: name: material + custom_dir: docs/overrides palette: - - scheme: default + - media: "(prefers-color-scheme)" + toggle: + icon: material/lightbulb-auto + name: Switch to light mode + - media: '(prefers-color-scheme: light)' + scheme: default primary: deep purple accent: amber toggle: icon: material/lightbulb name: Switch to dark mode - - scheme: slate + - media: '(prefers-color-scheme: dark)' + scheme: slate primary: deep purple accent: amber toggle: icon: material/lightbulb-outline - name: Switch to light mode + name: Switch to system preference features: - - search.suggest - - search.highlight + - content.code.annotate + - content.code.copy + # - content.code.select + - content.footnote.tooltips - content.tabs.link + - content.tooltips + - navigation.footer + - navigation.indexes + - navigation.instant + - navigation.instant.prefetch + # - navigation.instant.preview + - navigation.instant.progress + - navigation.path + - navigation.tabs + - navigation.tabs.sticky + - navigation.top + - navigation.tracking + - search.highlight + - search.share + - search.suggest + - toc.follow + icon: repo: fontawesome/brands/github-alt logo: img/icon-white.svg favicon: img/favicon.png language: en -repo_name: tiangolo/sqlmodel -repo_url: https://github.com/tiangolo/sqlmodel -edit_uri: '' +repo_name: fastapi/sqlmodel +repo_url: https://github.com/fastapi/sqlmodel +plugins: + # Material for MkDocs + search: + social: + typeset: + # Other plugins + macros: + include_yaml: + - sponsors: data/sponsors.yml + - members: data/members.yml + nav: - SQLModel: index.md - features.md - - databases.md - - db-to-code.md - - Tutorial - User Guide: - - tutorial/index.md - - tutorial/create-db-and-table-with-db-browser.md - - tutorial/create-db-and-table.md - - tutorial/insert.md - - tutorial/automatic-id-none-refresh.md - - tutorial/select.md - - tutorial/where.md - - tutorial/indexes.md - - tutorial/one.md - - tutorial/limit-and-offset.md - - tutorial/update.md - - tutorial/delete.md - - Connect Tables - JOIN: - - tutorial/connect/index.md - - tutorial/connect/create-connected-tables.md - - tutorial/connect/create-connected-rows.md - - tutorial/connect/read-connected-data.md - - tutorial/connect/update-data-connections.md - - tutorial/connect/remove-data-connections.md - - Relationship Attributes: - - tutorial/relationship-attributes/index.md - - tutorial/relationship-attributes/define-relationships-attributes.md - - tutorial/relationship-attributes/create-and-update-relationships.md - - tutorial/relationship-attributes/read-relationships.md - - tutorial/relationship-attributes/remove-relationships.md - - tutorial/relationship-attributes/back-populates.md - - tutorial/relationship-attributes/type-annotation-strings.md - - Many to Many: - - tutorial/many-to-many/index.md - - tutorial/many-to-many/create-models-with-link.md - - tutorial/many-to-many/create-data.md - - tutorial/many-to-many/update-remove-relationships.md - - tutorial/many-to-many/link-with-extra-fields.md - - tutorial/code-structure.md - - FastAPI and Pydantic: - - tutorial/fastapi/index.md - - tutorial/fastapi/simple-hero-api.md - - tutorial/fastapi/response-model.md - - tutorial/fastapi/multiple-models.md - - tutorial/fastapi/read-one.md - - tutorial/fastapi/limit-and-offset.md - - tutorial/fastapi/update.md - - tutorial/fastapi/delete.md - - tutorial/fastapi/session-with-dependency.md - - tutorial/fastapi/teams.md - - tutorial/fastapi/relationships.md - - tutorial/fastapi/tests.md - - Advanced User Guide: - - advanced/index.md - - advanced/decimal.md - - alternatives.md - - help.md - - contributing.md + - Learn: + - learn/index.md + - databases.md + - db-to-code.md + - environment-variables.md + - virtual-environments.md + - install.md + - Tutorial - User Guide: + - tutorial/index.md + - tutorial/create-db-and-table-with-db-browser.md + - tutorial/create-db-and-table.md + - tutorial/insert.md + - tutorial/automatic-id-none-refresh.md + - tutorial/select.md + - tutorial/where.md + - tutorial/indexes.md + - tutorial/one.md + - tutorial/limit-and-offset.md + - tutorial/update.md + - tutorial/delete.md + - Connect Tables - JOIN: + - tutorial/connect/index.md + - tutorial/connect/create-connected-tables.md + - tutorial/connect/create-connected-rows.md + - tutorial/connect/read-connected-data.md + - tutorial/connect/update-data-connections.md + - tutorial/connect/remove-data-connections.md + - Relationship Attributes: + - tutorial/relationship-attributes/index.md + - tutorial/relationship-attributes/define-relationships-attributes.md + - tutorial/relationship-attributes/create-and-update-relationships.md + - tutorial/relationship-attributes/read-relationships.md + - tutorial/relationship-attributes/remove-relationships.md + - tutorial/relationship-attributes/back-populates.md + - tutorial/relationship-attributes/cascade-delete-relationships.md + - tutorial/relationship-attributes/type-annotation-strings.md + - Many to Many: + - tutorial/many-to-many/index.md + - tutorial/many-to-many/create-models-with-link.md + - tutorial/many-to-many/create-data.md + - tutorial/many-to-many/update-remove-relationships.md + - tutorial/many-to-many/link-with-extra-fields.md + - tutorial/code-structure.md + - FastAPI and Pydantic: + - tutorial/fastapi/index.md + - tutorial/fastapi/simple-hero-api.md + - tutorial/fastapi/response-model.md + - tutorial/fastapi/multiple-models.md + - tutorial/fastapi/read-one.md + - tutorial/fastapi/limit-and-offset.md + - tutorial/fastapi/update.md + - tutorial/fastapi/update-extra-data.md + - tutorial/fastapi/delete.md + - tutorial/fastapi/session-with-dependency.md + - tutorial/fastapi/teams.md + - tutorial/fastapi/relationships.md + - tutorial/fastapi/tests.md + - Advanced User Guide: + - advanced/index.md + - advanced/decimal.md + - advanced/uuid.md + - Resources: + - resources/index.md + - help.md + - contributing.md + - management-tasks.md + - About: + - about/index.md + - alternatives.md + - management.md - release-notes.md markdown_extensions: -- toc: + # Material for MkDocs + material.extensions.preview: + targets: + include: + - "*" + # Python Markdown + abbr: + attr_list: + footnotes: + md_in_html: + tables: + toc: permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- admonition -- codehilite -- extra -- pymdownx.superfences: + + # Python Markdown Extensions + pymdownx.betterem: + smart_enable: all + pymdownx.caret: + pymdownx.highlight: + line_spans: __span + pymdownx.inlinehilite: + pymdownx.keys: + pymdownx.mark: + pymdownx.superfences: custom_fences: - name: mermaid class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- mdx_include + format: !!python/name:pymdownx.superfences.fence_code_format + pymdownx.tilde: + + # pymdownx blocks + pymdownx.blocks.admonition: + types: + - note + - attention + - caution + - danger + - error + - tip + - hint + - warning + # Custom types + - info + pymdownx.blocks.details: + pymdownx.blocks.tab: + alternate_style: True + + # Other extensions + mdx_include: + markdown_include_variants: extra: - analytics: - provider: google - property: UA-205713594-2 social: - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/sqlmodel + link: https://github.com/fastapi/sqlmodel - icon: fontawesome/brands/twitter link: https://twitter.com/tiangolo - icon: fontawesome/brands/linkedin @@ -131,3 +214,6 @@ extra_css: extra_javascript: - js/termynal.js - js/custom.js + +hooks: + - scripts/mkdocs_hooks.py diff --git a/pdm_build.py b/pdm_build.py new file mode 100644 index 0000000000..33a6b267e4 --- /dev/null +++ b/pdm_build.py @@ -0,0 +1,39 @@ +import os +from typing import Any + +from pdm.backend.hooks import Context + +TIANGOLO_BUILD_PACKAGE = os.getenv("TIANGOLO_BUILD_PACKAGE", "sqlmodel") + + +def pdm_build_initialize(context: Context) -> None: + metadata = context.config.metadata + # Get custom config for the current package, from the env var + config: dict[str, Any] = context.config.data["tool"]["tiangolo"][ + "_internal-slim-build" + ]["packages"][TIANGOLO_BUILD_PACKAGE] + project_config: dict[str, Any] = config["project"] + # Get main optional dependencies, extras + optional_dependencies: dict[str, list[str]] = metadata.get( + "optional-dependencies", {} + ) + # Get custom optional dependencies name to always include in this (non-slim) package + include_optional_dependencies: list[str] = config.get( + "include-optional-dependencies", [] + ) + # Override main [project] configs with custom configs for this package + for key, value in project_config.items(): + metadata[key] = value + # Get custom build config for the current package + build_config: dict[str, Any] = ( + config.get("tool", {}).get("pdm", {}).get("build", {}) + ) + # Override PDM build config with custom build config for this package + for key, value in build_config.items(): + context.config.build_config[key] = value + # Get main dependencies + dependencies: list[str] = metadata.get("dependencies", []) + # Add optional dependencies to the default dependencies for this (non-slim) package + for include_optional in include_optional_dependencies: + optional_dependencies_group = optional_dependencies.get(include_optional, []) + dependencies.extend(optional_dependencies_group) diff --git a/pyproject.toml b/pyproject.toml index 814cba6f7b..2bdbe67c15 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,26 +1,32 @@ -[tool.poetry] +[build-system] +requires = ["pdm-backend"] +build-backend = "pdm.backend" + +[project] name = "sqlmodel" -version = "0" +dynamic = ["version"] description = "SQLModel, SQL databases in Python, designed for simplicity, compatibility, and robustness." -authors = ["Sebastián Ramírez "] readme = "README.md" -homepage = "https://github.com/tiangolo/sqlmodel" -documentation = "https://sqlmodel.tiangolo.com" -repository = "https://github.com/tiangolo/sqlmodel" +requires-python = ">=3.9" +authors = [ + { name = "Sebastián Ramírez", email = "tiangolo@gmail.com" }, +] license = "MIT" -exclude = ["sqlmodel/sql/expression.py.jinja2"] +license-files = ["LICENSE"] + classifiers = [ "Development Status :: 4 - Beta", "Framework :: AsyncIO", "Intended Audience :: Developers", "Intended Audience :: Science/Research", "Intended Audience :: System Administrators", - "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Database", "Topic :: Database :: Database Engines/Servers", "Topic :: Internet", @@ -29,44 +35,58 @@ classifiers = [ "Typing :: Typed", ] -[tool.poetry.dependencies] -python = "^3.6.1" -SQLAlchemy = ">=1.4.17,<1.5.0" -pydantic = "^1.8.2" -sqlalchemy2-stubs = {version = "*", allow-prereleases = true} - -[tool.poetry.dev-dependencies] -pytest = "^6.2.4" -mypy = "0.930" -flake8 = "^3.9.2" -black = {version = "^21.5-beta.1", python = "^3.7"} -mkdocs = "^1.2.1" -mkdocs-material = "^8.1.4" -mdx-include = "^1.4.1" -coverage = {extras = ["toml"], version = "^5.5"} -fastapi = "^0.68.0" -requests = "^2.26.0" -autoflake = "^1.4" -isort = "^5.9.3" -async_generator = {version = "*", python = "~3.6"} -async-exit-stack = {version = "*", python = "~3.6"} +dependencies = [ + "SQLAlchemy >=2.0.14,<2.1.0", + "pydantic>=2.7.0", +] + +[project.urls] +Homepage = "https://github.com/fastapi/sqlmodel" +Documentation = "https://sqlmodel.tiangolo.com" +Repository = "https://github.com/fastapi/sqlmodel" +Issues = "https://github.com/fastapi/sqlmodel/issues" +Changelog = "https://sqlmodel.tiangolo.com/release-notes/" -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" +[tool.pdm] +version = { source = "file", path = "sqlmodel/__init__.py" } +distribution = true + +[tool.pdm.build] +source-includes = [ + "tests/", + "docs_src/", + "requirements*.txt", + "scripts/", + "sqlmodel/sql/expression.py.jinja2", + ] + +[tool.tiangolo._internal-slim-build.packages.sqlmodel-slim.project] +name = "sqlmodel-slim" -[tool.poetry-version-plugin] -source = "init" +[tool.tiangolo._internal-slim-build.packages.sqlmodel] +# include-optional-dependencies = ["standard"] + +[tool.tiangolo._internal-slim-build.packages.sqlmodel.project] +optional-dependencies = {} + +# [tool.tiangolo._internal-slim-build.packages.sqlmodel.project.scripts] +# sqlmodel = "sqlmodel.cli:main" [tool.coverage.run] parallel = true +data_file = "coverage/.coverage" source = [ "docs_src", "tests", "sqlmodel" ] +context = '${CONTEXT}' +dynamic_context = "test_function" +relative_files = true [tool.coverage.report] +show_missing = true +sort = "-Cover" exclude_lines = [ "pragma: no cover", "@overload", @@ -74,31 +94,42 @@ exclude_lines = [ "if TYPE_CHECKING:", ] -[tool.isort] -profile = "black" -known_third_party = ["sqlmodel"] -skip_glob = [ - "sqlmodel/__init__.py", - ] - +[tool.coverage.html] +show_contexts = true [tool.mypy] -# --strict -disallow_any_generics = true -disallow_subclassing_any = true -disallow_untyped_calls = true -disallow_untyped_defs = true -disallow_incomplete_defs = true -check_untyped_defs = true -disallow_untyped_decorators = true -no_implicit_optional = true -warn_redundant_casts = true -warn_unused_ignores = true -warn_return_any = true -implicit_reexport = false -strict_equality = true -# --strict end +strict = true +exclude = "sqlmodel.sql._expression_select_gen" [[tool.mypy.overrides]] -module = "sqlmodel.sql.expression" -warn_unused_ignores = false +module = "docs_src.*" +disallow_incomplete_defs = false +disallow_untyped_defs = false +disallow_untyped_calls = false + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "UP", # pyupgrade +] +ignore = [ + "E501", # line too long, handled by black + "B008", # do not perform function calls in argument defaults + "C901", # too complex + "W191", # indentation contains tabs +] + +[tool.ruff.lint.per-file-ignores] +# "__init__.py" = ["F401"] + +[tool.ruff.lint.isort] +known-third-party = ["sqlmodel", "sqlalchemy", "pydantic", "fastapi"] + +[tool.ruff.lint.pyupgrade] +# Preserve types, even if a file imports `from __future__ import annotations`. +keep-runtime-typing = true diff --git a/requirements-docs-tests.txt b/requirements-docs-tests.txt new file mode 100644 index 0000000000..c65317a7cf --- /dev/null +++ b/requirements-docs-tests.txt @@ -0,0 +1,2 @@ +# For mkdocstrings and code generator using templates +black >=22.10 diff --git a/requirements-docs.txt b/requirements-docs.txt new file mode 100644 index 0000000000..b35f75d894 --- /dev/null +++ b/requirements-docs.txt @@ -0,0 +1,19 @@ +-e . +-r requirements-docs-tests.txt +mkdocs-material==9.7.1 +mdx-include >=1.4.1,<2.0.0 +mkdocs-redirects>=1.2.1,<1.3.0 +pyyaml >=5.3.1,<7.0.0 +# For Material for MkDocs, Chinese search +# jieba==0.42.1 +# For image processing by Material for MkDocs +pillow==11.3.0 +# For image processing by Material for MkDocs +cairosvg==2.8.2 +mkdocstrings[python]==0.30.1 +griffe-typingdoc==0.3.0 +griffe-warnings-deprecated==1.1.0 +# For griffe, it formats with black +typer == 0.21.0 +mkdocs-macros-plugin==1.5.0 +markdown-include-variants==0.0.8 diff --git a/requirements-github-actions.txt b/requirements-github-actions.txt new file mode 100644 index 0000000000..5c3e02d8ae --- /dev/null +++ b/requirements-github-actions.txt @@ -0,0 +1,5 @@ +PyGithub>=2.3.0,<3.0.0 +pydantic>=2.5.3,<3.0.0 +pydantic-settings>=2.1.0,<3.0.0 +httpx>=0.27.0,<0.29.0 +smokeshow diff --git a/requirements-tests.txt b/requirements-tests.txt new file mode 100644 index 0000000000..b61b8429e6 --- /dev/null +++ b/requirements-tests.txt @@ -0,0 +1,12 @@ +-e . +-r requirements-docs-tests.txt +pytest >=7.0.1,<9.0.0 +coverage[toml] >=6.2,<8.0 +mypy ==1.19.1 +ruff ==0.14.10 +# For FastAPI tests +fastapi >=0.103.2,<0.129.0 +httpx ==0.28.1 +dirty-equals ==0.11 +jinja2 ==3.1.6 +typing-extensions ==4.15.0 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000..63212c3507 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +-e . + +-r requirements-tests.txt +-r requirements-docs.txt + +prek>=0.2.24,<1.0.0 diff --git a/scripts/deploy_docs_status.py b/scripts/deploy_docs_status.py new file mode 100644 index 0000000000..635711bdbf --- /dev/null +++ b/scripts/deploy_docs_status.py @@ -0,0 +1,127 @@ +import logging +import re +from typing import Literal + +from github import Auth, Github +from pydantic import BaseModel, SecretStr +from pydantic_settings import BaseSettings + +site_domain = "sqlmodel.tiangolo.com" + + +class Settings(BaseSettings): + github_repository: str + github_token: SecretStr + deploy_url: str | None = None + commit_sha: str + run_id: int + state: Literal["pending", "success", "error"] = "pending" + + +class LinkData(BaseModel): + previous_link: str + preview_link: str + + +def main() -> None: + logging.basicConfig(level=logging.INFO) + settings = Settings() + + logging.info(f"Using config: {settings.model_dump_json()}") + g = Github(auth=Auth.Token(settings.github_token.get_secret_value())) + repo = g.get_repo(settings.github_repository) + use_pr = next( + (pr for pr in repo.get_pulls() if pr.head.sha == settings.commit_sha), None + ) + if not use_pr: + logging.error(f"No PR found for hash: {settings.commit_sha}") + return + commits = list(use_pr.get_commits()) + current_commit = [c for c in commits if c.sha == settings.commit_sha][0] + run_url = f"https://github.com/{settings.github_repository}/actions/runs/{settings.run_id}" + if settings.state == "pending": + current_commit.create_status( + state="pending", + description="Deploying Docs", + context="deploy-docs", + target_url=run_url, + ) + logging.info("No deploy URL available yet") + return + if settings.state == "error": + current_commit.create_status( + state="error", + description="Error Deploying Docs", + context="deploy-docs", + target_url=run_url, + ) + logging.info("Error deploying docs") + return + assert settings.state == "success" + if not settings.deploy_url: + current_commit.create_status( + state="success", + description="No Docs Changes", + context="deploy-docs", + target_url=run_url, + ) + logging.info("No docs changes found") + return + assert settings.deploy_url + current_commit.create_status( + state="success", + description="Docs Deployed", + context="deploy-docs", + target_url=run_url, + ) + + files = list(use_pr.get_files()) + docs_files = [f for f in files if f.filename.startswith("docs/")] + + deploy_url = settings.deploy_url.rstrip("/") + links: list[LinkData] = [] + for f in docs_files: + match = re.match(r"docs/(.*)", f.filename) + if not match: + continue + path = match.group(1) + if path.endswith("index.md"): + use_path = path.replace("index.md", "") + else: + use_path = path.replace(".md", "/") + link = LinkData( + previous_link=f"https://{site_domain}/{use_path}", + preview_link=f"{deploy_url}/{use_path}", + ) + links.append(link) + links.sort(key=lambda x: x.preview_link) + + header = "## 📝 Docs preview" + message = header + message += f"\n\nLast commit {settings.commit_sha} at: {deploy_url}" + + if links: + message += "\n\n### Modified Pages\n\n" + for link in links: + message += f"* {link.preview_link}" + message += f" - ([before]({link.previous_link}))" + message += "\n" + + print(message) + issue = use_pr.as_issue() + comments = list(issue.get_comments()) + for comment in comments: + if ( + comment.body.startswith(header) + and comment.user.login == "github-actions[bot]" + ): + comment.edit(message) + break + else: + issue.create_comment(message) + + logging.info("Finished") + + +if __name__ == "__main__": + main() diff --git a/scripts/docs-live.sh b/scripts/docs-live.sh deleted file mode 100755 index 5342a9e59f..0000000000 --- a/scripts/docs-live.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -set -e - -mkdocs serve --dev-addr 127.0.0.1:8008 diff --git a/scripts/docs.py b/scripts/docs.py new file mode 100644 index 0000000000..a424f177b4 --- /dev/null +++ b/scripts/docs.py @@ -0,0 +1,147 @@ +import logging +import os +import re +import subprocess +from http.server import HTTPServer, SimpleHTTPRequestHandler +from pathlib import Path + +import mkdocs.utils +import typer +from jinja2 import Template + +logging.basicConfig(level=logging.INFO) + +mkdocs_name = "mkdocs.yml" +en_docs_path = Path("") + +app = typer.Typer() + + +@app.callback() +def callback() -> None: + # For MacOS with Cairo + os.environ["DYLD_FALLBACK_LIBRARY_PATH"] = "/opt/homebrew/lib" + + +index_sponsors_template = """ +{% if sponsors %} +{% for sponsor in sponsors.gold -%} + +{% endfor -%} +{%- for sponsor in sponsors.silver -%} + +{% endfor %} +{% endif %} +""" + + +def generate_readme_content() -> str: + en_index = en_docs_path / "docs" / "index.md" + content = en_index.read_text("utf-8") + match_pre = re.search(r"\n\n", content) + match_start = re.search(r"", content) + match_end = re.search(r"", content) + sponsors_data_path = en_docs_path / "data" / "sponsors.yml" + sponsors = mkdocs.utils.yaml_load(sponsors_data_path.read_text(encoding="utf-8")) + if not (match_start and match_end): + raise RuntimeError("Couldn't auto-generate sponsors section") + if not match_pre: + raise RuntimeError("Couldn't find pre section (