Merge branch 'main' into main

This commit is contained in:
Sebastián Ramírez 2023-10-29 11:55:17 +04:00 committed by GitHub
commit a1833901f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
165 changed files with 3744 additions and 3153 deletions

View File

@ -1,5 +1,3 @@
name: Question or Problem
description: Ask a question or ask about a problem
labels: [question] labels: [question]
body: body:
- type: markdown - type: markdown
@ -9,9 +7,9 @@ body:
Please follow these instructions, fill every question, and do every step. 🙏 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'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 issues. 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. 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.
@ -21,16 +19,16 @@ body:
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. 😎 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 - type: checkboxes
id: checks id: checks
attributes: attributes:
label: First Check label: First Check
description: Please confirm and check all the following options. description: Please confirm and check all the following options.
options: options:
- label: I added a very descriptive title to this issue. - label: I added a very descriptive title here.
required: true 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 required: true
- label: I searched the SQLModel documentation, with the integrated search. - label: I searched the SQLModel documentation, with the integrated search.
required: true required: true
@ -51,7 +49,7 @@ body:
* Read open issues with questions until I find 2 issues where I can help someone and add a comment to help there. * 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. * 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: options:
- label: I commit to help with one of those options 👆 - label: I commit to help with one of those options 👆

View File

@ -2,3 +2,12 @@ blank_issues_enabled: false
contact_links: contact_links:
- name: Security Contact - name: Security Contact
about: Please report security vulnerabilities to security@tiangolo.com 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/tiangolo/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/tiangolo/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/tiangolo/sqlmodel/discussions/categories/show-and-tell

View File

@ -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.

22
.github/ISSUE_TEMPLATE/privileged.yml vendored Normal file
View File

@ -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/tiangolo/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.

View File

@ -48,9 +48,7 @@ if __name__ == "__main__":
use_pr = pr use_pr = pr
break break
if not use_pr: if not use_pr:
logging.error( logging.error(f"No PR found for hash: {event.workflow_run.head_commit.id}")
f"No PR found for hash: {event.workflow_run.head_commit.id}"
)
sys.exit(0) sys.exit(0)
github_headers = { github_headers = {
"Authorization": f"token {settings.input_token.get_secret_value()}" "Authorization": f"token {settings.input_token.get_secret_value()}"

View File

@ -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"]

View File

@ -1,10 +0,0 @@
name: Watch docs previews in PRs
description: Check PRs and trigger new docs deploys
author: "Sebastián Ramírez <tiangolo@gmail.com>"
inputs:
token:
description: 'Token for the repo. Can be passed in using {{ secrets.GITHUB_TOKEN }}'
required: true
runs:
using: docker
image: Dockerfile

View File

@ -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")

View File

@ -4,78 +4,91 @@ on:
branches: branches:
- main - main
pull_request: pull_request:
types: [opened, synchronize] types:
workflow_dispatch: - opened
inputs: - synchronize
debug_enabled:
description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
required: false
default: false
jobs: 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@v4
# For pull requests it's not necessary to checkout the code but for the main branch it is
- uses: dorny/paths-filter@v2
id: filter
with:
filters: |
docs:
- README.md
- docs/**
- docs_src/**
- pyproject.toml
- mkdocs.yml
- mkdocs.insiders.yml
build-docs: build-docs:
runs-on: ubuntu-20.04 needs:
- changes
if: ${{ needs.changes.outputs.docs == 'true' }}
runs-on: ubuntu-latest
steps: steps:
- name: Dump GitHub context - name: Dump GitHub context
env: env:
GITHUB_CONTEXT: ${{ toJson(github) }} GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT" run: echo "$GITHUB_CONTEXT"
- uses: actions/checkout@v3.1.0 - uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
python-version: "3.7" python-version: "3.11"
# 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@v3 - uses: actions/cache@v3
id: cache id: cache
with: with:
path: ${{ env.pythonLocation }} path: ${{ env.pythonLocation }}
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-root-docs key: ${{ runner.os }}-python-docs-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-v01
- name: Install poetry - name: Install Poetry
if: steps.cache.outputs.cache-hit != 'true' 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: | run: |
python -m pip install --upgrade pip 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"
python -m pip install "poetry==1.2.0a2" python -m poetry self add poetry-version-plugin
python -m poetry plugin add poetry-version-plugin
- name: Configure poetry - name: Configure poetry
run: python -m poetry config virtualenvs.create false run: python -m poetry config virtualenvs.create false
- name: Install Dependencies - name: Install Dependencies
if: steps.cache.outputs.cache-hit != 'true' if: steps.cache.outputs.cache-hit != 'true'
run: python -m poetry install run: python -m poetry install
- name: Install Material for MkDocs Insiders - name: Install Material for MkDocs Insiders
if: github.event.pull_request.head.repo.fork == false && steps.cache.outputs.cache-hit != 'true' if: ( github.event_name != 'pull_request' || 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 run: python -m poetry run pip install git+https://${{ secrets.SQLMODEL_MKDOCS_MATERIAL_INSIDERS }}@github.com/squidfunk/mkdocs-material-insiders.git
- uses: actions/cache@v3 - uses: actions/cache@v3
with: with:
key: mkdocs-cards-${{ github.ref }} key: mkdocs-cards-${{ github.ref }}
path: .cache path: .cache
- name: Build Docs - name: Build Docs
if: github.event.pull_request.head.repo.fork == true if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true
run: python -m poetry run mkdocs build run: python -m poetry run mkdocs build
- name: Build Docs with Insiders - name: Build Docs with Insiders
if: github.event.pull_request.head.repo.fork == false if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false
run: python -m poetry run mkdocs build --config-file mkdocs.insiders.yml 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@v3 - uses: actions/upload-artifact@v3
with: with:
name: docs-zip name: docs-site
path: ./site/docs.zip path: ./site/**
- name: Deploy to Netlify
uses: nwtgck/actions-netlify@v1.1.5 # 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: with:
publish-dir: './site' jobs: ${{ toJSON(needs) }}
production-branch: main allowed-skips: build-docs
github-token: ${{ secrets.GITHUB_TOKEN }}
enable-commit-comment: false
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}

48
.github/workflows/deploy-docs.yml vendored Normal file
View File

@ -0,0 +1,48 @@
name: Deploy Docs
on:
workflow_run:
workflows:
- Build Docs
types:
- completed
jobs:
deploy-docs:
runs-on: ubuntu-latest
steps:
- name: Dump GitHub context
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- uses: actions/checkout@v4
- name: Clean site
run: |
rm -rf ./site
mkdir ./site
- name: Download Artifact Docs
id: download
uses: dawidd6/action-download-artifact@v2.28.0
with:
if_no_artifact_found: ignore
github_token: ${{ secrets.GITHUB_TOKEN }}
workflow: build-docs.yml
run_id: ${{ github.event.workflow_run.id }}
name: docs-site
path: ./site/
- name: Deploy to Cloudflare Pages
if: steps.download.outputs.found_artifact == 'true'
id: deploy
uses: cloudflare/pages-action@v1
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: sqlmodel
directory: './site'
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
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 ) }}
- name: Comment Deploy
if: steps.deploy.outputs.url != ''
uses: ./.github/actions/comment-docs-preview-in-pr
with:
token: ${{ secrets.GITHUB_TOKEN }}
deploy_url: "${{ steps.deploy.outputs.url }}"

View File

@ -14,20 +14,20 @@ on:
debug_enabled: 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 required: false
default: false default: 'false'
jobs: jobs:
latest-changes: latest-changes:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3.1.0 - uses: actions/checkout@v4
with: with:
# To allow latest-changes to commit to the main branch # To allow latest-changes to commit to the main branch
token: ${{ secrets.ACTIONS_TOKEN }} token: ${{ secrets.SQLMODEL_LATEST_CHANGES }}
# Allow debugging with tmate # Allow debugging with tmate
- name: Setup tmate session - name: Setup tmate session
uses: mxschmitt/action-tmate@v3 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: with:
limit-access-to-actor: true limit-access-to-actor: true
- uses: docker://tiangolo/latest-changes:0.0.3 - uses: docker://tiangolo/latest-changes:0.0.3

View File

@ -1,46 +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@v3.1.0
- name: Clean site
run: |
rm -rf ./site
mkdir ./site
- name: Download Artifact Docs
uses: dawidd6/action-download-artifact@v2.24.2
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
workflow: build-docs.yml
run_id: ${{ github.event.workflow_run.id }}
name: docs-zip
path: ./site/
- name: Unzip docs
run: |
cd ./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 }}"

View File

@ -9,13 +9,13 @@ on:
debug_enabled: 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 required: false
default: false default: 'false'
jobs: jobs:
publish: publish:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3.1.0 - uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
@ -23,24 +23,20 @@ jobs:
# Allow debugging with tmate # Allow debugging with tmate
- name: Setup tmate session - name: Setup tmate session
uses: mxschmitt/action-tmate@v3 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: with:
limit-access-to-actor: true limit-access-to-actor: true
- uses: actions/cache@v3 - uses: actions/cache@v3
id: cache id: cache
with: with:
path: ${{ env.pythonLocation }} path: ${{ env.pythonLocation }}
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-root key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-root-v2
- name: Install poetry - name: Install poetry
if: steps.cache.outputs.cache-hit != 'true' 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: | run: |
python -m pip install --upgrade pip 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"
python -m pip install "poetry==1.2.0a2" python -m poetry self add poetry-version-plugin
python -m poetry plugin add poetry-version-plugin
- name: Configure poetry - name: Configure poetry
run: python -m poetry config virtualenvs.create false run: python -m poetry config virtualenvs.create false
- name: Install Dependencies - name: Install Dependencies

View File

@ -20,7 +20,7 @@ jobs:
- run: pip install smokeshow - run: pip install smokeshow
- uses: dawidd6/action-download-artifact@v2.24.2 - uses: dawidd6/action-download-artifact@v2.28.0
with: with:
workflow: test.yml workflow: test.yml
commit: ${{ github.event.workflow_run.head_sha }} commit: ${{ github.event.workflow_run.head_sha }}

View File

@ -5,24 +5,30 @@ on:
branches: branches:
- main - main
pull_request: pull_request:
types: [opened, synchronize] types:
- opened
- synchronize
workflow_dispatch: workflow_dispatch:
inputs: inputs:
debug_enabled: 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 required: false
default: false default: 'false'
jobs: jobs:
test: test:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python-version: ["3.6.15", "3.7", "3.8", "3.9", "3.10"] python-version:
- "3.7"
- "3.8"
- "3.9"
- "3.10"
fail-fast: false fail-fast: false
steps: steps:
- uses: actions/checkout@v3.1.0 - uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
@ -30,31 +36,26 @@ jobs:
# Allow debugging with tmate # Allow debugging with tmate
- name: Setup tmate session - name: Setup tmate session
uses: mxschmitt/action-tmate@v3 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: with:
limit-access-to-actor: true limit-access-to-actor: true
- uses: actions/cache@v3 - uses: actions/cache@v3
id: cache id: cache
with: with:
path: ${{ env.pythonLocation }} path: ${{ env.pythonLocation }}
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-root key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-root-v2
- name: Install poetry - name: Install poetry
if: steps.cache.outputs.cache-hit != 'true' 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: | run: |
python -m pip install --upgrade pip 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"
python -m pip install "poetry==1.2.0a2" python -m poetry self add poetry-version-plugin
python -m poetry plugin add poetry-version-plugin
- name: Configure poetry - name: Configure poetry
run: python -m poetry config virtualenvs.create false run: python -m poetry config virtualenvs.create false
- name: Install Dependencies - name: Install Dependencies
if: steps.cache.outputs.cache-hit != 'true' if: steps.cache.outputs.cache-hit != 'true'
run: python -m poetry install run: python -m poetry install
- name: Lint - name: Lint
if: ${{ matrix.python-version != '3.6.15' }}
run: python -m poetry run bash scripts/lint.sh run: python -m poetry run bash scripts/lint.sh
- run: mkdir coverage - run: mkdir coverage
- name: Test - name: Test
@ -68,11 +69,12 @@ jobs:
name: coverage name: coverage
path: coverage path: coverage
coverage-combine: coverage-combine:
needs: [test] needs:
- test
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
with: with:
@ -96,3 +98,15 @@ jobs:
with: with:
name: coverage-html name: coverage-html
path: htmlcov path: htmlcov
# 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) }}

25
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,25 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
default_language_version:
python: python3.10
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: check-added-large-files
- id: check-toml
- id: check-yaml
args:
- --unsafe
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.1.2
hooks:
- id: ruff
args:
- --fix
- id: ruff-format
ci:
autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
autoupdate_commit_msg: ⬆ [pre-commit.ci] pre-commit autoupdate

24
CITATION.cff Normal file
View File

@ -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/tiangolo/sqlmodel'
url: 'https://sqlmodel.tiangolo.com'
abstract: >-
SQLModel, SQL databases in Python, designed for
simplicity, compatibility, and robustness.
keywords:
- fastapi
- pydantic
- sqlalchemy
license: MIT

View File

@ -50,7 +50,7 @@ It combines SQLAlchemy and Pydantic and tries to simplify the code you write as
## Requirements ## Requirements
A recent and currently supported version of Python (right now, <a href="https://www.python.org/downloads/" class="external-link" target="_blank">Python supports versions 3.6 and above</a>). A recent and currently supported <a href="https://www.python.org/downloads/" class="external-link" target="_blank">version of Python</a>.
As **SQLModel** is based on **Pydantic** and **SQLAlchemy**, it requires them. They will be automatically installed when you install SQLModel. As **SQLModel** is based on **Pydantic** and **SQLAlchemy**, it requires them. They will be automatically installed when you install SQLModel.

View File

@ -6,10 +6,6 @@ First, you might want to see the basic ways to [help SQLModel and get help](help
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 repository and you know that you need to deep dive in the code, here are some guidelines to set up your environment.
### Python
SQLModel supports Python 3.6 and above, but for development you should have at least **Python 3.7**.
### Poetry ### Poetry
**SQLModel** uses <a href="https://python-poetry.org/" class="external-link" target="_blank">Poetry</a> to build, package, and publish the project. **SQLModel** uses <a href="https://python-poetry.org/" class="external-link" target="_blank">Poetry</a> to build, package, and publish the project.
@ -116,7 +112,7 @@ There is a script that you can run locally to test all the code and generate cov
<div class="termy"> <div class="termy">
```console ```console
$ bash scripts/test-cov-html.sh $ bash scripts/test.sh
``` ```
</div> </div>

View File

@ -111,7 +111,7 @@ DROP TABLE hero;
That is how you tell the database in SQL to delete the entire table `hero`. That is how you tell the database in SQL to delete the entire table `hero`.
<a href="http://www.nooooooooooooooo.com/" class="external-link" target="_blank">Nooooo!</a> We lost all the data in the `hero` table! 💥😱 <a href="https://theuselessweb.site/nooooooooooooooo/" class="external-link" target="_blank">Nooooo!</a> We lost all the data in the `hero` table! 💥😱
### SQL Sanitization ### SQL Sanitization

View File

@ -12,7 +12,7 @@ Nevertheless, SQLModel is completely **independent** of FastAPI and can be used
## Just Modern Python ## Just Modern Python
It's all based on standard <abbr title="Python currently supported versions, 3.6 and above.">modern **Python**</abbr> type annotations. No new syntax to learn. Just standard modern Python. It's all based on standard <abbr title="Currently supported versions of Python">modern **Python**</abbr> 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: <a href="https://fastapi.tiangolo.com/python-types/" class="external-link" target="_blank">Python types intro</a>. 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: <a href="https://fastapi.tiangolo.com/python-types/" class="external-link" target="_blank">Python types intro</a>.

View File

@ -58,26 +58,125 @@ You can:
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. 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 <a href="https://github.com/tiangolo/sqlmodel/issues" class="external-link" target="_blank">existing issues</a> 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:
* <a href="https://github.com/tiangolo/sqlmodel/discussions/categories/questions?discussions_q=category%3AQuestions+is%3Aunanswered" class="external-link" target="_blank">GitHub Discussions</a>
* <a href="https://github.com/tiangolo/sqlmodel/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Aquestion+-label%3Aanswered+" class="external-link" target="_blank">GitHub Issues</a>
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 <a href="https://stackoverflow.com/help/minimal-reproducible-example" class="external-link" target="_blank">minimal, reproducible, example</a>, 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 ## Watch the GitHub repository
You can "watch" SQLModel in GitHub (clicking the "watch" button at the top right): <a href="https://github.com/tiangolo/sqlmodel" class="external-link" target="_blank">https://github.com/tiangolo/sqlmodel</a>. 👀 You can "watch" SQLModel in GitHub (clicking the "watch" button at the top right): <a href="https://github.com/tiangolo/sqlmodel" class="external-link" target="_blank">https://github.com/tiangolo/sqlmodel</a>. 👀
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 <a href="https://github.com/tiangolo/sqlmodel/issues/new/choose" class="external-link" target="_blank">create a new issue</a> in the GitHub repository, for example to: You can <a href="https://github.com/tiangolo/sqlmodel/discussions/new?category=questions" class="external-link" target="_blank">create a new question</a> in the GitHub repository, for example to:
* Ask a **question** or ask about a **problem**. * Ask a **question** or ask about a **problem**.
* Suggest a new **feature**. * 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 ## Create a Pull Request
@ -86,7 +185,44 @@ You can [contribute](contributing.md){.internal-link target=_blank} to the sourc
* To fix a typo you found on the documentation. * To fix a typo you found on the documentation.
* To propose new documentation sections. * To propose new documentation sections.
* To fix an existing issue/bug. * To fix an existing issue/bug.
* Make sure to add tests.
* To add a new feature. * 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 👥 <a href="https://discord.gg/VQjSZaeJmf" class="external-link" target="_blank">FastAPI and Friends Discord chat server</a> 👥 and hang out with others in the community. There's a `#sqlmodel` channel.
!!! tip
For questions, ask them in <a href="https://github.com/tiangolo/sqlmodel/discussions/new?category=questions" class="external-link" target="_blank">GitHub Discussions</a>, 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 ## Sponsor the author

View File

@ -50,7 +50,7 @@ It combines SQLAlchemy and Pydantic and tries to simplify the code you write as
## Requirements ## Requirements
A recent and currently supported version of Python (right now, <a href="https://www.python.org/downloads/" class="external-link" target="_blank">Python supports versions 3.6 and above</a>). A recent and currently supported <a href="https://www.python.org/downloads/" class="external-link" target="_blank">version of Python</a>.
As **SQLModel** is based on **Pydantic** and **SQLAlchemy**, it requires them. They will be automatically installed when you install SQLModel. As **SQLModel** is based on **Pydantic** and **SQLAlchemy**, it requires them. They will be automatically installed when you install SQLModel.
@ -68,7 +68,7 @@ Successfully installed sqlmodel
## Example ## Example
For an introduction to databases, SQL, and everything else, see the <a href="https://sqlmodel.tiangolo.com" target="_blank">SQLModel documentation</a>. For an introduction to databases, SQL, and everything else, see the <a href="https://sqlmodel.tiangolo.com/databases/" target="_blank">SQLModel documentation</a>.
Here's a quick example. ✨ Here's a quick example. ✨

View File

@ -2,13 +2,81 @@
## Latest Changes ## Latest Changes
* 👷 Refactor CI artifact upload/download for docs previews. PR [#514](https://github.com/tiangolo/sqlmodel/pull/514) by [@tiangolo](https://github.com/tiangolo). * 🎨 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).
* ✨ 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).
## 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 internal function name `get_sqlachemy_type()`. PR [#496](https://github.com/tiangolo/sqlmodel/pull/496) by [@cmarqu](https://github.com/cmarqu).
* ⬆ Bump actions/cache from 2 to 3. PR [#497](https://github.com/tiangolo/sqlmodel/pull/497) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ✏️ Fix typo in docs. PR [#446](https://github.com/tiangolo/sqlmodel/pull/446) by [@davidbrochart](https://github.com/davidbrochart). * ✏️ Fix typo in docs. PR [#446](https://github.com/tiangolo/sqlmodel/pull/446) by [@davidbrochart](https://github.com/davidbrochart).
* ⬆ 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).
* ✏️ 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 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). * ✏️ 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). * 🔧 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). * 👷 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). * ⬆ 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).
@ -28,7 +96,7 @@
### Fixes ### 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). * 🐛 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). * ♻️ 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 ### Docs

View File

@ -36,7 +36,7 @@ When we create a new `Hero` instance, we don't set the `id`:
{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py[ln:23-26]!} {!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py[ln:23-26]!}
# Code below ommitted 👇 # Code below omitted 👇
``` ```
<details> <details>
@ -125,7 +125,7 @@ We can verify by creating a session using a `with` block and adding the objects.
{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py[ln:23-41]!} {!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py[ln:23-41]!}
# Code below ommitted 👇 # Code below omitted 👇
``` ```
<details> <details>
@ -238,7 +238,7 @@ To confirm and understand how this **automatic expiration and refresh** of data
{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py[ln:33-58]!} {!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py[ln:33-58]!}
# Code below ommitted 👇 # Code below omitted 👇
``` ```
<details> <details>
@ -271,7 +271,7 @@ Let's see how it works:
```console ```console
$ python app.py $ python app.py
// Output above ommitted 👆 // Output above omitted 👆
// After committing, the objects are expired and have no values // After committing, the objects are expired and have no values
After committing the session After committing the session
@ -335,7 +335,7 @@ You can do that too with `session.refresh(object)`:
{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py[ln:33-67]!} {!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py[ln:33-67]!}
# Code below ommitted 👇 # Code below omitted 👇
``` ```
<details> <details>
@ -362,7 +362,7 @@ Here's how the output would look like:
```console ```console
$ python app.py $ python app.py
// Output above ommitted 👆 // Output above omitted 👆
// The first refresh // The first refresh
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age
@ -427,7 +427,7 @@ And the output shows again the same data:
```console ```console
$ python app.py $ 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 // 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 INFO Engine ROLLBACK

View File

@ -168,7 +168,7 @@ Let's assume that now the file structure is:
### Circular Imports and Type Annotations ### Circular Imports and Type Annotations
The problem with circular imports is that Python can't resolve them at <abbr title="While it is executing the program, as oposed to the code as just text in a file stored on disk.">*runtime*</abbr>. The problem with circular imports is that Python can't resolve them at <abbr title="While it is executing the program, as opposed to the code as just text in a file stored on disk.">*runtime*</abbr>.
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.

View File

@ -106,7 +106,7 @@ This is the same model we have been using up to now, we are just adding the new
Most of that should look familiar: 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. We add a default of `None` to the `Field()` so we don't have to explicitly pass `team_id=None` when creating a hero.

View File

@ -164,6 +164,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 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. 🔍 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. 🔍

View File

@ -220,7 +220,7 @@ Each supported database has it's own URL type. For example, for **SQLite** it is
* `sqlite:///databases/local/application.db` * `sqlite:///databases/local/application.db`
* `sqlite:///db.sqlite` * `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://` * `sqlite://`

View File

@ -42,7 +42,7 @@ We want to allow clients to set different `offset` and `limit` values.
But we don't want them to be able to set a `limit` of something like `9999`, that's over `9000`! 😱 But we don't want them to be able to set a `limit` of something like `9999`, that's over `9000`! 😱
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`. 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`.
This way, a client can decide to take fewer heroes if they want, but not more. This way, a client can decide to take fewer heroes if they want, but not more.

View File

@ -53,11 +53,11 @@ 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 `Optional[int]`, 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 ultimate goal of an API is for some **clients to use it**.

View File

@ -84,7 +84,7 @@ In this case, we used `response_model=TeamRead` and `response_model=HeroRead`, s
# Code here omitted 👈 # Code here omitted 👈
{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:159-164]!} {!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:158-163]!}
# Code below omitted 👇 # Code below omitted 👇
``` ```
@ -234,7 +234,7 @@ In the case of the hero, this tells FastAPI to extract the `team` too. And in th
# Code here omitted 👈 # Code here omitted 👈
{!./docs_src/tutorial/fastapi/relationships/tutorial001.py[ln:168-173]!} {!./docs_src/tutorial/fastapi/relationships/tutorial001.py[ln:167-172]!}
# Code below omitted 👇 # Code below omitted 👇
``` ```

View File

@ -177,7 +177,7 @@ And then we remove the previous `with` block with the old **session**.
# Code here omitted 👈 # Code here omitted 👈
{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:55-107]!} {!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:55-106]!}
``` ```
<details> <details>

View File

@ -92,7 +92,7 @@ These are equivalent and very similar to the **path operations** for the **heroe
```Python hl_lines="3-9 12-20 23-28 31-47 50-57" ```Python hl_lines="3-9 12-20 23-28 31-47 50-57"
# Code above omitted 👆 # Code above omitted 👆
{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:139-193]!} {!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:138-192]!}
# Code below omitted 👇 # Code below omitted 👇
``` ```

View File

@ -82,7 +82,7 @@ But now, we need to deal with a bit of logistics and details we are not paying a
This test looks fine, but there's a problem. 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 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. So, we should use an independent **testing database**, just for the tests.

View File

@ -64,15 +64,13 @@ $ cd sqlmodel-tutorial
Make sure you have an officially supported version of 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: You can check which version you have with:
<div class="termy"> <div class="termy">
```console ```console
$ python3 --version $ python3 --version
Python 3.6.9 Python 3.11
``` ```
</div> </div>
@ -84,8 +82,6 @@ You might want to try with the specific versions, for example with:
* `python3.10` * `python3.10`
* `python3.9` * `python3.9`
* `python3.8` * `python3.8`
* `python3.7`
* `python3.6`
The code would look like this: The code would look like this:
@ -139,7 +135,7 @@ Here are the commands you could use:
// Remember that you might need to use python3.9 or similar 💡 // Remember that you might need to use python3.9 or similar 💡
// Create the virtual environment using the module "venv" // Create the virtual environment using the module "venv"
$ python3 -m venv env $ python3 -m venv env
// ...here it creates the virtual enviroment in the directory "env" // ...here it creates the virtual environment in the directory "env"
// Activate the virtual environment // Activate the virtual environment
$ source ./env/bin/activate $ source ./env/bin/activate
// Verify that the virtual environment is active // Verify that the virtual environment is active
@ -161,7 +157,7 @@ Here are the commands you could use:
```console ```console
// Create the virtual environment using the module "venv" // Create the virtual environment using the module "venv"
# >$ python3 -m venv env # >$ python3 -m venv env
// ...here it creates the virtual enviroment in the directory "env" // ...here it creates the virtual environment in the directory "env"
// Activate the virtual environment // Activate the virtual environment
# >$ .\env\Scripts\Activate.ps1 # >$ .\env\Scripts\Activate.ps1
// Verify that the virtual environment is active // Verify that the virtual environment is active

View File

@ -171,7 +171,7 @@ The first step is to import the `Session` class:
```Python hl_lines="3" ```Python hl_lines="3"
{!./docs_src/tutorial/insert/tutorial001.py[ln:1-3]!} {!./docs_src/tutorial/insert/tutorial001.py[ln:1-3]!}
# Code below ommitted 👇 # Code below omitted 👇
``` ```
<details> <details>

View File

@ -271,11 +271,11 @@ Of course, you can also combine `.limit()` and `.offset()` with `.where()` and o
</details> </details>
## 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. 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:
<div class="termy"> <div class="termy">
@ -284,18 +284,17 @@ $ python app.py
// Previous output omitted 🙈 // Previous output omitted 🙈
// Select with WHERE and LIMIT // Select with WHERE and 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 FROM hero
WHERE hero.age > ? WHERE hero.age > ?
LIMIT ? OFFSET ? 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, id=6, name='Dr. Weird', secret_name='Steve Weird'),
Hero(age=36, secret_name='Steve Weird', id=6, name='Dr. Weird'), Hero(age=48, id=3, name='Rusty-Man', secret_name='Tommy Sharp')
Hero(age=48, secret_name='Tommy Sharp', id=3, name='Rusty-Man')
] ]
``` ```

View File

@ -179,4 +179,4 @@ INFO Engine ROLLBACK
## Recap ## 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. ✨

View File

@ -12,7 +12,7 @@ Let's see the utilities to read a single row.
## Continue From Previous Code ## 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.
<details> <details>
<summary>👀 Full file preview</summary> <summary>👀 Full file preview</summary>

View File

@ -123,7 +123,7 @@ Now let's update **Spider-Boy**, removing him from the team by setting `hero_spi
</details> </details>
The first important thing is, we *haven't commited* the hero yet, so accessing the list of heroes would not trigger an automatic refresh. 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**. 🔥 But in our code, in this exact point in time, we already said that **Spider-Boy** is no longer part of the **Preventers**. 🔥

View File

@ -115,9 +115,7 @@ This means that this attribute could be `None`, or it could be a full `Team` obj
This is because the related **`team_id` could also be `None`** (or `NULL` in the database). 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]`. If it was required for a `Hero` instance to belong to a `Team`, then the `team_id` would be `int` instead of `Optional[int]`, 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 `Optional[Team]`.
And the `team` attribute would be a `Team` instead of `Optional[Team]`.
## Relationship Attributes With Lists ## Relationship Attributes With Lists

View File

@ -52,7 +52,7 @@ With what we have learned **up to now**, we could use a `select()` statement, th
## Get Relationship Team - New Way ## 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: So, the highlighted block above, has the same results as the block below:

View File

@ -190,7 +190,7 @@ First we have to import `select` from `sqlmodel` at the top of the file:
```Python hl_lines="3" ```Python hl_lines="3"
{!./docs_src/tutorial/select/tutorial001.py[ln:1-3]!} {!./docs_src/tutorial/select/tutorial001.py[ln:1-3]!}
# More code below ommitted 👇 # More code below omitted 👇
``` ```
<details> <details>
@ -472,7 +472,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()`. 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. ✨ 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. ✨

View File

@ -206,7 +206,7 @@ We care specially about the **select** statement:
## Filter Rows Using `WHERE` with **SQLModel** ## 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" ```Python hl_lines="5"
# Code above omitted 👆 # Code above omitted 👆
@ -748,7 +748,7 @@ FROM hero
WHERE hero.age >= ? AND hero.age < ? WHERE hero.age >= ? AND hero.age < ?
INFO Engine [no key 0.00014s] (35, 40) 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=35 id=5 name='Black Lion' secret_name='Trevor Challa'
age=36 id=6 name='Dr. Weird' secret_name='Steve Weird' age=36 id=6 name='Dr. Weird' secret_name='Steve Weird'

View File

@ -157,7 +157,7 @@
Hero 3: 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: Generates the output:

View File

@ -21,56 +21,56 @@ def create_db_and_tables():
def create_heroes(): def create_heroes():
hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") # (1) hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") # (1)!
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") # (2) hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") # (2)!
hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) # (3) hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) # (3)!
print("Before interacting with the database") # (4) print("Before interacting with the database") # (4)!
print("Hero 1:", hero_1) # (5) print("Hero 1:", hero_1) # (5)!
print("Hero 2:", hero_2) # (6) print("Hero 2:", hero_2) # (6)!
print("Hero 3:", hero_3) # (7) print("Hero 3:", hero_3) # (7)!
with Session(engine) as session: # (8) with Session(engine) as session: # (8)!
session.add(hero_1) # (9) session.add(hero_1) # (9)!
session.add(hero_2) # (10) session.add(hero_2) # (10)!
session.add(hero_3) # (11) session.add(hero_3) # (11)!
print("After adding to the session") # (12) print("After adding to the session") # (12)!
print("Hero 1:", hero_1) # (13) print("Hero 1:", hero_1) # (13)!
print("Hero 2:", hero_2) # (14) print("Hero 2:", hero_2) # (14)!
print("Hero 3:", hero_3) # (15) print("Hero 3:", hero_3) # (15)!
session.commit() # (16) session.commit() # (16)!
print("After committing the session") # (17) print("After committing the session") # (17)!
print("Hero 1:", hero_1) # (18) print("Hero 1:", hero_1) # (18)!
print("Hero 2:", hero_2) # (19) print("Hero 2:", hero_2) # (19)!
print("Hero 3:", hero_3) # (20) print("Hero 3:", hero_3) # (20)!
print("After committing the session, show IDs") # (21) print("After committing the session, show IDs") # (21)!
print("Hero 1 ID:", hero_1.id) # (22) print("Hero 1 ID:", hero_1.id) # (22)!
print("Hero 2 ID:", hero_2.id) # (23) print("Hero 2 ID:", hero_2.id) # (23)!
print("Hero 3 ID:", hero_3.id) # (24) print("Hero 3 ID:", hero_3.id) # (24)!
print("After committing the session, show names") # (25) print("After committing the session, show names") # (25)!
print("Hero 1 name:", hero_1.name) # (26) print("Hero 1 name:", hero_1.name) # (26)!
print("Hero 2 name:", hero_2.name) # (27) print("Hero 2 name:", hero_2.name) # (27)!
print("Hero 3 name:", hero_3.name) # (28) print("Hero 3 name:", hero_3.name) # (28)!
session.refresh(hero_1) # (29) session.refresh(hero_1) # (29)!
session.refresh(hero_2) # (30) session.refresh(hero_2) # (30)!
session.refresh(hero_3) # (31) session.refresh(hero_3) # (31)!
print("After refreshing the heroes") # (32) print("After refreshing the heroes") # (32)!
print("Hero 1:", hero_1) # (33) print("Hero 1:", hero_1) # (33)!
print("Hero 2:", hero_2) # (34) print("Hero 2:", hero_2) # (34)!
print("Hero 3:", hero_3) # (35) print("Hero 3:", hero_3) # (35)!
# (36) # (36)!
print("After the session closes") # (37) print("After the session closes") # (37)!
print("Hero 1:", hero_1) # (38) print("Hero 1:", hero_1) # (38)!
print("Hero 2:", hero_2) # (39) print("Hero 2:", hero_2) # (39)!
print("Hero 3:", hero_3) # (40) print("Hero 3:", hero_3) # (40)!
def main(): def main():

View File

@ -1,24 +1,24 @@
from typing import Optional # (1) from typing import Optional # (1)!
from sqlmodel import Field, SQLModel, create_engine # (2) from sqlmodel import Field, SQLModel, create_engine # (2)!
class Hero(SQLModel, table=True): # (3) class Hero(SQLModel, table=True): # (3)!
id: Optional[int] = Field(default=None, primary_key=True) # (4) id: Optional[int] = Field(default=None, primary_key=True) # (4)!
name: str # (5) name: str # (5)!
secret_name: str # (6) secret_name: str # (6)!
age: Optional[int] = None # (7) age: Optional[int] = None # (7)!
sqlite_file_name = "database.db" # (8) sqlite_file_name = "database.db" # (8)!
sqlite_url = f"sqlite:///{sqlite_file_name}" # (9) sqlite_url = f"sqlite:///{sqlite_file_name}" # (9)!
engine = create_engine(sqlite_url, echo=True) # (10) engine = create_engine(sqlite_url, echo=True) # (10)!
def create_db_and_tables(): # (11) def create_db_and_tables(): # (11)!
SQLModel.metadata.create_all(engine) # (12) SQLModel.metadata.create_all(engine) # (12)!
if __name__ == "__main__": # (13) if __name__ == "__main__": # (13)!
create_db_and_tables() # (14) create_db_and_tables() # (14)!

View File

@ -71,23 +71,23 @@ def update_heroes():
def delete_heroes(): def delete_heroes():
with Session(engine) as session: with Session(engine) as session:
statement = select(Hero).where(Hero.name == "Spider-Youngster") # (1) statement = select(Hero).where(Hero.name == "Spider-Youngster") # (1)!
results = session.exec(statement) # (2) results = session.exec(statement) # (2)!
hero = results.one() # (3) hero = results.one() # (3)!
print("Hero: ", hero) # (4) print("Hero: ", hero) # (4)!
session.delete(hero) # (5) session.delete(hero) # (5)!
session.commit() # (6) session.commit() # (6)!
print("Deleted hero:", hero) # (7) print("Deleted hero:", hero) # (7)!
statement = select(Hero).where(Hero.name == "Spider-Youngster") # (8) statement = select(Hero).where(Hero.name == "Spider-Youngster") # (8)!
results = session.exec(statement) # (9) results = session.exec(statement) # (9)!
hero = results.first() # (10) hero = results.first() # (10)!
if hero is None: # (11) if hero is None: # (11)!
print("There's no hero named Spider-Youngster") # (12) print("There's no hero named Spider-Youngster") # (12)!
# (13) # (13)!
def main(): def main():

View File

@ -66,7 +66,7 @@ def read_heroes(
*, *,
session: Session = Depends(get_session), session: Session = Depends(get_session),
offset: int = 0, 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() heroes = session.exec(select(Hero).offset(offset).limit(limit)).all()
return heroes return heroes
@ -98,7 +98,6 @@ def update_hero(
@app.delete("/heroes/{hero_id}") @app.delete("/heroes/{hero_id}")
def delete_hero(*, session: Session = Depends(get_session), hero_id: int): def delete_hero(*, session: Session = Depends(get_session), hero_id: int):
hero = session.get(Hero, hero_id) hero = session.get(Hero, hero_id)
if not hero: if not hero:
raise HTTPException(status_code=404, detail="Hero not found") raise HTTPException(status_code=404, detail="Hero not found")

View File

@ -1,7 +1,7 @@
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from sqlmodel import Session, SQLModel, create_engine 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(): def test_create_hero():
@ -17,16 +17,16 @@ def test_create_hero():
app.dependency_overrides[get_session] = 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"} "/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"}
) )
app.dependency_overrides.clear() app.dependency_overrides.clear()
data = response.json() # (4) data = response.json() # (4)!
assert response.status_code == 200 # (5) assert response.status_code == 200 # (5)!
assert data["name"] == "Deadpond" # (6) assert data["name"] == "Deadpond" # (6)!
assert data["secret_name"] == "Dive Wilson" # (7) assert data["secret_name"] == "Dive Wilson" # (7)!
assert data["age"] is None # (8) assert data["age"] is None # (8)!
assert data["id"] is not None # (9) assert data["id"] is not None # (9)!

View File

@ -1,7 +1,7 @@
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from sqlmodel import Session, SQLModel, create_engine 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(): def test_create_hero():
@ -12,17 +12,17 @@ def test_create_hero():
with Session(engine) as session: with Session(engine) as session:
def get_session_override(): # (2) def get_session_override(): # (2)!
return session # (3) return session # (3)!
app.dependency_overrides[get_session] = get_session_override # (4) app.dependency_overrides[get_session] = get_session_override # (4)!
client = TestClient(app) client = TestClient(app)
response = client.post( response = client.post(
"/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"} "/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"}
) )
app.dependency_overrides.clear() # (5) app.dependency_overrides.clear() # (5)!
data = response.json() data = response.json()
assert response.status_code == 200 assert response.status_code == 200

View File

@ -1,21 +1,21 @@
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from sqlmodel import Session, SQLModel, create_engine 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(): def test_create_hero():
engine = create_engine( # (2) engine = create_engine( # (2)!
"sqlite:///testing.db", connect_args={"check_same_thread": False} "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(): 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) client = TestClient(app)
@ -30,4 +30,4 @@ def test_create_hero():
assert data["secret_name"] == "Dive Wilson" assert data["secret_name"] == "Dive Wilson"
assert data["age"] is None assert data["age"] is None
assert data["id"] is not None assert data["id"] is not None
# (6) # (6)!

View File

@ -1,15 +1,15 @@
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from sqlmodel import Session, SQLModel, create_engine 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 from .main import app, get_session
def test_create_hero(): def test_create_hero():
engine = create_engine( engine = create_engine(
"sqlite://", # (2) "sqlite://", # (2)!
connect_args={"check_same_thread": False}, connect_args={"check_same_thread": False},
poolclass=StaticPool, # (3) poolclass=StaticPool, # (3)!
) )
SQLModel.metadata.create_all(engine) SQLModel.metadata.create_all(engine)

View File

@ -1,4 +1,4 @@
import pytest # (1) import pytest # (1)!
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from sqlmodel import Session, SQLModel, create_engine from sqlmodel import Session, SQLModel, create_engine
from sqlmodel.pool import StaticPool from sqlmodel.pool import StaticPool
@ -6,19 +6,19 @@ from sqlmodel.pool import StaticPool
from .main import app, get_session from .main import app, get_session
@pytest.fixture(name="session") # (2) @pytest.fixture(name="session") # (2)!
def session_fixture(): # (3) def session_fixture(): # (3)!
engine = create_engine( engine = create_engine(
"sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool "sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool
) )
SQLModel.metadata.create_all(engine) SQLModel.metadata.create_all(engine)
with Session(engine) as session: 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(): def get_session_override():
return session # (6) return session # (6)!
app.dependency_overrides[get_session] = get_session_override app.dependency_overrides[get_session] = get_session_override

View File

@ -16,19 +16,19 @@ def session_fixture():
yield session yield session
@pytest.fixture(name="client") # (1) @pytest.fixture(name="client") # (1)!
def client_fixture(session: Session): # (2) def client_fixture(session: Session): # (2)!
def get_session_override(): # (3) def get_session_override(): # (3)!
return session return session
app.dependency_overrides[get_session] = get_session_override # (4) app.dependency_overrides[get_session] = get_session_override # (4)!
client = TestClient(app) # (5) client = TestClient(app) # (5)!
yield client # (6) yield client # (6)!
app.dependency_overrides.clear() # (7) app.dependency_overrides.clear() # (7)!
def test_create_hero(client: TestClient): # (8) def test_create_hero(client: TestClient): # (8)!
response = client.post( response = client.post(
"/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"} "/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"}
) )

View File

@ -58,7 +58,7 @@ def create_hero(hero: HeroCreate):
@app.get("/heroes/", response_model=List[HeroRead]) @app.get("/heroes/", response_model=List[HeroRead])
def read_heroes(offset: int = 0, limit: int = Query(default=100, lte=100)): def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)):
with Session(engine) as session: with Session(engine) as session:
heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() heroes = session.exec(select(Hero).offset(offset).limit(limit)).all()
return heroes return heroes

View File

@ -52,7 +52,7 @@ def create_hero(hero: HeroCreate):
@app.get("/heroes/", response_model=List[HeroRead]) @app.get("/heroes/", response_model=List[HeroRead])
def read_heroes(offset: int = 0, limit: int = Query(default=100, lte=100)): def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)):
with Session(engine) as session: with Session(engine) as session:
heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() heroes = session.exec(select(Hero).offset(offset).limit(limit)).all()
return heroes return heroes

View File

@ -104,7 +104,7 @@ def read_heroes(
*, *,
session: Session = Depends(get_session), session: Session = Depends(get_session),
offset: int = 0, 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() heroes = session.exec(select(Hero).offset(offset).limit(limit)).all()
return heroes return heroes
@ -136,7 +136,6 @@ def update_hero(
@app.delete("/heroes/{hero_id}") @app.delete("/heroes/{hero_id}")
def delete_hero(*, session: Session = Depends(get_session), hero_id: int): def delete_hero(*, session: Session = Depends(get_session), hero_id: int):
hero = session.get(Hero, hero_id) hero = session.get(Hero, hero_id)
if not hero: if not hero:
raise HTTPException(status_code=404, detail="Hero not found") raise HTTPException(status_code=404, detail="Hero not found")
@ -159,7 +158,7 @@ def read_teams(
*, *,
session: Session = Depends(get_session), session: Session = Depends(get_session),
offset: int = 0, 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() teams = session.exec(select(Team).offset(offset).limit(limit)).all()
return teams return teams

View File

@ -66,7 +66,7 @@ def read_heroes(
*, *,
session: Session = Depends(get_session), session: Session = Depends(get_session),
offset: int = 0, 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() heroes = session.exec(select(Hero).offset(offset).limit(limit)).all()
return heroes return heroes
@ -98,7 +98,6 @@ def update_hero(
@app.delete("/heroes/{hero_id}") @app.delete("/heroes/{hero_id}")
def delete_hero(*, session: Session = Depends(get_session), hero_id: int): def delete_hero(*, session: Session = Depends(get_session), hero_id: int):
hero = session.get(Hero, hero_id) hero = session.get(Hero, hero_id)
if not hero: if not hero:
raise HTTPException(status_code=404, detail="Hero not found") raise HTTPException(status_code=404, detail="Hero not found")

View File

@ -95,7 +95,7 @@ def read_heroes(
*, *,
session: Session = Depends(get_session), session: Session = Depends(get_session),
offset: int = 0, 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() heroes = session.exec(select(Hero).offset(offset).limit(limit)).all()
return heroes return heroes
@ -127,7 +127,6 @@ def update_hero(
@app.delete("/heroes/{hero_id}") @app.delete("/heroes/{hero_id}")
def delete_hero(*, session: Session = Depends(get_session), hero_id: int): def delete_hero(*, session: Session = Depends(get_session), hero_id: int):
hero = session.get(Hero, hero_id) hero = session.get(Hero, hero_id)
if not hero: if not hero:
raise HTTPException(status_code=404, detail="Hero not found") raise HTTPException(status_code=404, detail="Hero not found")
@ -150,7 +149,7 @@ def read_teams(
*, *,
session: Session = Depends(get_session), session: Session = Depends(get_session),
offset: int = 0, 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() teams = session.exec(select(Team).offset(offset).limit(limit)).all()
return teams return teams

View File

@ -58,7 +58,7 @@ def create_hero(hero: HeroCreate):
@app.get("/heroes/", response_model=List[HeroRead]) @app.get("/heroes/", response_model=List[HeroRead])
def read_heroes(offset: int = 0, limit: int = Query(default=100, lte=100)): def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)):
with Session(engine) as session: with Session(engine) as session:
heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() heroes = session.exec(select(Hero).offset(offset).limit(limit)).all()
return heroes return heroes

View File

@ -20,24 +20,24 @@ def create_db_and_tables():
SQLModel.metadata.create_all(engine) SQLModel.metadata.create_all(engine)
def create_heroes(): # (1) def create_heroes(): # (1)!
hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") # (2) hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") # (2)!
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48)
with Session(engine) as session: # (3) with Session(engine) as session: # (3)!
session.add(hero_1) # (4) session.add(hero_1) # (4)!
session.add(hero_2) session.add(hero_2)
session.add(hero_3) session.add(hero_3)
session.commit() # (5) session.commit() # (5)!
# (6) # (6)!
def main(): # (7) def main(): # (7)!
create_db_and_tables() # (8) create_db_and_tables() # (8)!
create_heroes() # (9) create_heroes() # (9)!
if __name__ == "__main__": # (10) if __name__ == "__main__": # (10)!
main() # (11) main() # (11)!

View File

@ -43,7 +43,7 @@ def create_heroes():
def select_heroes(): def select_heroes():
with Session(engine) as session: 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) results = session.exec(statement)
heroes = results.all() heroes = results.all()
print(heroes) print(heroes)

View File

@ -1,9 +1,9 @@
from typing import Optional 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) id: Optional[int] = Field(default=None, primary_key=True)
name: str name: str
secret_name: str secret_name: str
@ -13,19 +13,19 @@ class Hero(SQLModel, table=True): # (2)
sqlite_file_name = "database.db" sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}" 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(): def create_db_and_tables():
SQLModel.metadata.create_all(engine) # (4) SQLModel.metadata.create_all(engine) # (4)!
def create_heroes(): 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_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) 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_1)
session.add(hero_2) session.add(hero_2)
session.add(hero_3) session.add(hero_3)
@ -34,18 +34,18 @@ def create_heroes():
def select_heroes(): def select_heroes():
with Session(engine) as session: # (7) with Session(engine) as session: # (7)!
statement = select(Hero) # (8) statement = select(Hero) # (8)!
results = session.exec(statement) # (9) results = session.exec(statement) # (9)!
for hero in results: # (10) for hero in results: # (10)!
print(hero) # (11) print(hero) # (11)!
# (12) # (12)!
def main(): def main():
create_db_and_tables() create_db_and_tables()
create_heroes() create_heroes()
select_heroes() # (13) select_heroes() # (13)!
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -132,7 +132,7 @@
!!! tip !!! tip
SQLAlchemy is still using the previous transaction, so it doesn't have to create a new one. 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: This generates the output:

View File

@ -43,16 +43,16 @@ def create_heroes():
def update_heroes(): def update_heroes():
with Session(engine) as session: with Session(engine) as session:
statement = select(Hero).where(Hero.name == "Spider-Boy") # (1) statement = select(Hero).where(Hero.name == "Spider-Boy") # (1)!
results = session.exec(statement) # (2) results = session.exec(statement) # (2)!
hero = results.one() # (3) hero = results.one() # (3)!
print("Hero:", hero) # (4) print("Hero:", hero) # (4)!
hero.age = 16 # (5) hero.age = 16 # (5)!
session.add(hero) # (6) session.add(hero) # (6)!
session.commit() # (7) session.commit() # (7)!
session.refresh(hero) # (8) session.refresh(hero) # (8)!
print("Updated hero:", hero) # (9) print("Updated hero:", hero) # (9)!
def main(): def main():

View File

@ -43,31 +43,31 @@ def create_heroes():
def update_heroes(): def update_heroes():
with Session(engine) as session: with Session(engine) as session:
statement = select(Hero).where(Hero.name == "Spider-Boy") # (1) statement = select(Hero).where(Hero.name == "Spider-Boy") # (1)!
results = session.exec(statement) # (2) results = session.exec(statement) # (2)!
hero_1 = results.one() # (3) hero_1 = results.one() # (3)!
print("Hero 1:", hero_1) # (4) print("Hero 1:", hero_1) # (4)!
statement = select(Hero).where(Hero.name == "Captain North America") # (5) statement = select(Hero).where(Hero.name == "Captain North America") # (5)!
results = session.exec(statement) # (6) results = session.exec(statement) # (6)!
hero_2 = results.one() # (7) hero_2 = results.one() # (7)!
print("Hero 2:", hero_2) # (8) print("Hero 2:", hero_2) # (8)!
hero_1.age = 16 # (9) hero_1.age = 16 # (9)!
hero_1.name = "Spider-Youngster" # (10) hero_1.name = "Spider-Youngster" # (10)!
session.add(hero_1) # (11) session.add(hero_1) # (11)!
hero_2.name = "Captain North America Except Canada" # (12) hero_2.name = "Captain North America Except Canada" # (12)!
hero_2.age = 110 # (13) hero_2.age = 110 # (13)!
session.add(hero_2) # (14) session.add(hero_2) # (14)!
session.commit() # (15) session.commit() # (15)!
session.refresh(hero_1) # (16) session.refresh(hero_1) # (16)!
session.refresh(hero_2) # (17) session.refresh(hero_2) # (17)!
print("Updated hero 1:", hero_1) # (18) print("Updated hero 1:", hero_1) # (18)!
print("Updated hero 2:", hero_2) # (19) print("Updated hero 2:", hero_2) # (19)!
# (20) # (20)!
def main(): def main():

View File

@ -110,7 +110,7 @@ markdown_extensions:
extra: extra:
analytics: analytics:
provider: google provider: google
property: UA-205713594-2 property: G-J8HVTT936W
social: social:
- icon: fontawesome/brands/github-alt - icon: fontawesome/brands/github-alt
link: https://github.com/tiangolo/sqlmodel link: https://github.com/tiangolo/sqlmodel

View File

@ -17,10 +17,10 @@ classifiers = [
"Intended Audience :: System Administrators", "Intended Audience :: System Administrators",
"License :: OSI Approved :: MIT License", "License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Topic :: Database", "Topic :: Database",
"Topic :: Database :: Database Engines/Servers", "Topic :: Database :: Database Engines/Servers",
"Topic :: Internet", "Topic :: Internet",
@ -30,28 +30,24 @@ classifiers = [
] ]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.6.1" python = "^3.7"
SQLAlchemy = ">=1.4.17,<=1.4.41" SQLAlchemy = ">=1.4.36,<2.0.0"
pydantic = "^1.8.2" pydantic = "^1.9.0"
sqlalchemy2-stubs = {version = "*", allow-prereleases = true} sqlalchemy2-stubs = {version = "*", allow-prereleases = true}
[tool.poetry.dev-dependencies] [tool.poetry.group.dev.dependencies]
pytest = "^7.0.1" pytest = "^7.0.1"
mypy = "0.971" mypy = "0.971"
flake8 = "^5.0.4" # Needed by the code generator using templates
black = {version = "^22.10.0", python = "^3.7"} black = "^22.10.0"
mkdocs = "^1.2.1" mkdocs-material = "9.1.21"
mkdocs-material = "^8.1.4" pillow = "^9.3.0"
pillow = {version = "^9.3.0", python = "^3.7"} cairosvg = "^2.5.2"
cairosvg = {version = "^2.5.2", python = "^3.7"}
mdx-include = "^1.4.1" mdx-include = "^1.4.1"
coverage = {extras = ["toml"], version = "^6.2"} coverage = {extras = ["toml"], version = "^6.2"}
fastapi = "^0.68.1" fastapi = "^0.68.1"
requests = "^2.26.0" requests = "^2.26.0"
autoflake = "^1.4" ruff = "^0.1.2"
isort = "^5.9.3"
async_generator = {version = "*", python = "~3.6"}
async-exit-stack = {version = "*", python = "~3.6"}
[build-system] [build-system]
requires = ["poetry-core"] requires = ["poetry-core"]
@ -77,33 +73,36 @@ exclude_lines = [
"if TYPE_CHECKING:", "if TYPE_CHECKING:",
] ]
[tool.isort]
profile = "black"
known_third_party = ["sqlmodel"]
skip_glob = [
"sqlmodel/__init__.py",
]
[tool.mypy] [tool.mypy]
# --strict strict = true
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
[[tool.mypy.overrides]] [[tool.mypy.overrides]]
module = "sqlmodel.sql.expression" module = "sqlmodel.sql.expression"
warn_unused_ignores = false warn_unused_ignores = false
# invalidate CI cache: 1 [tool.ruff]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"C", # flake8-comprehensions
"B", # flake8-bugbear
"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.per-file-ignores]
# "__init__.py" = ["F401"]
[tool.ruff.isort]
known-third-party = ["sqlmodel", "sqlalchemy", "pydantic", "fastapi"]
[tool.ruff.pyupgrade]
# Preserve types, even if a file imports `from __future__ import annotations`.
keep-runtime-typing = true

View File

@ -2,4 +2,6 @@
set -e set -e
export DYLD_FALLBACK_LIBRARY_PATH="/opt/homebrew/lib"
mkdocs serve --dev-addr 127.0.0.1:8008 mkdocs serve --dev-addr 127.0.0.1:8008

View File

@ -1,6 +1,5 @@
#!/bin/sh -e #!/bin/sh -e
set -x set -x
autoflake --remove-all-unused-imports --recursive --remove-unused-variables --in-place sqlmodel docs_src tests --exclude=__init__.py ruff sqlmodel tests docs_src scripts --fix
black sqlmodel tests docs_src ruff format sqlmodel tests docs_src scripts
isort sqlmodel tests docs_src

View File

@ -4,8 +4,5 @@ set -e
set -x set -x
mypy sqlmodel mypy sqlmodel
flake8 sqlmodel tests docs_src ruff sqlmodel tests docs_src scripts
black sqlmodel tests docs_src --check ruff format sqlmodel tests docs_src --check
isort sqlmodel tests docs_src scripts --check-only
# TODO: move this to test.sh after deprecating Python 3.6
CHECK_JINJA=1 python scripts/generate_select.py

View File

@ -3,6 +3,7 @@
set -e set -e
set -x set -x
CHECK_JINJA=1 python scripts/generate_select.py
coverage run -m pytest tests coverage run -m pytest tests
coverage combine coverage combine
coverage report --show-missing coverage report --show-missing

View File

@ -1,16 +1,16 @@
__version__ = "0.0.8" __version__ = "0.0.10"
# Re-export from SQLAlchemy # Re-export from SQLAlchemy
from sqlalchemy.engine import create_mock_engine as create_mock_engine from sqlalchemy.engine import create_mock_engine as create_mock_engine
from sqlalchemy.engine import engine_from_config as engine_from_config from sqlalchemy.engine import engine_from_config as engine_from_config
from sqlalchemy.inspection import inspect as inspect from sqlalchemy.inspection import inspect as inspect
from sqlalchemy.schema import BLANK_SCHEMA as BLANK_SCHEMA from sqlalchemy.schema import BLANK_SCHEMA as BLANK_SCHEMA
from sqlalchemy.schema import DDL as DDL
from sqlalchemy.schema import CheckConstraint as CheckConstraint from sqlalchemy.schema import CheckConstraint as CheckConstraint
from sqlalchemy.schema import Column as Column from sqlalchemy.schema import Column as Column
from sqlalchemy.schema import ColumnDefault as ColumnDefault from sqlalchemy.schema import ColumnDefault as ColumnDefault
from sqlalchemy.schema import Computed as Computed from sqlalchemy.schema import Computed as Computed
from sqlalchemy.schema import Constraint as Constraint from sqlalchemy.schema import Constraint as Constraint
from sqlalchemy.schema import DDL as DDL
from sqlalchemy.schema import DefaultClause as DefaultClause from sqlalchemy.schema import DefaultClause as DefaultClause
from sqlalchemy.schema import FetchedValue as FetchedValue from sqlalchemy.schema import FetchedValue as FetchedValue
from sqlalchemy.schema import ForeignKey as ForeignKey from sqlalchemy.schema import ForeignKey as ForeignKey
@ -23,6 +23,14 @@ from sqlalchemy.schema import Sequence as Sequence
from sqlalchemy.schema import Table as Table from sqlalchemy.schema import Table as Table
from sqlalchemy.schema import ThreadLocalMetaData as ThreadLocalMetaData from sqlalchemy.schema import ThreadLocalMetaData as ThreadLocalMetaData
from sqlalchemy.schema import UniqueConstraint as UniqueConstraint from sqlalchemy.schema import UniqueConstraint as UniqueConstraint
from sqlalchemy.sql import LABEL_STYLE_DEFAULT as LABEL_STYLE_DEFAULT
from sqlalchemy.sql import (
LABEL_STYLE_DISAMBIGUATE_ONLY as LABEL_STYLE_DISAMBIGUATE_ONLY,
)
from sqlalchemy.sql import LABEL_STYLE_NONE as LABEL_STYLE_NONE
from sqlalchemy.sql import (
LABEL_STYLE_TABLENAME_PLUS_COL as LABEL_STYLE_TABLENAME_PLUS_COL,
)
from sqlalchemy.sql import alias as alias from sqlalchemy.sql import alias as alias
from sqlalchemy.sql import all_ as all_ from sqlalchemy.sql import all_ as all_
from sqlalchemy.sql import and_ as and_ from sqlalchemy.sql import and_ as and_
@ -48,14 +56,6 @@ from sqlalchemy.sql import insert as insert
from sqlalchemy.sql import intersect as intersect from sqlalchemy.sql import intersect as intersect
from sqlalchemy.sql import intersect_all as intersect_all from sqlalchemy.sql import intersect_all as intersect_all
from sqlalchemy.sql import join as join from sqlalchemy.sql import join as join
from sqlalchemy.sql import LABEL_STYLE_DEFAULT as LABEL_STYLE_DEFAULT
from sqlalchemy.sql import (
LABEL_STYLE_DISAMBIGUATE_ONLY as LABEL_STYLE_DISAMBIGUATE_ONLY,
)
from sqlalchemy.sql import LABEL_STYLE_NONE as LABEL_STYLE_NONE
from sqlalchemy.sql import (
LABEL_STYLE_TABLENAME_PLUS_COL as LABEL_STYLE_TABLENAME_PLUS_COL,
)
from sqlalchemy.sql import lambda_stmt as lambda_stmt from sqlalchemy.sql import lambda_stmt as lambda_stmt
from sqlalchemy.sql import lateral as lateral from sqlalchemy.sql import lateral as lateral
from sqlalchemy.sql import literal as literal from sqlalchemy.sql import literal as literal
@ -85,55 +85,53 @@ from sqlalchemy.sql import values as values
from sqlalchemy.sql import within_group as within_group from sqlalchemy.sql import within_group as within_group
from sqlalchemy.types import ARRAY as ARRAY from sqlalchemy.types import ARRAY as ARRAY
from sqlalchemy.types import BIGINT as BIGINT from sqlalchemy.types import BIGINT as BIGINT
from sqlalchemy.types import BigInteger as BigInteger
from sqlalchemy.types import BINARY as BINARY from sqlalchemy.types import BINARY as BINARY
from sqlalchemy.types import BLOB as BLOB from sqlalchemy.types import BLOB as BLOB
from sqlalchemy.types import BOOLEAN as BOOLEAN from sqlalchemy.types import BOOLEAN as BOOLEAN
from sqlalchemy.types import Boolean as Boolean
from sqlalchemy.types import CHAR as CHAR from sqlalchemy.types import CHAR as CHAR
from sqlalchemy.types import CLOB as CLOB from sqlalchemy.types import CLOB as CLOB
from sqlalchemy.types import DATE as DATE from sqlalchemy.types import DATE as DATE
from sqlalchemy.types import Date as Date
from sqlalchemy.types import DATETIME as DATETIME from sqlalchemy.types import DATETIME as DATETIME
from sqlalchemy.types import DateTime as DateTime
from sqlalchemy.types import DECIMAL as DECIMAL from sqlalchemy.types import DECIMAL as DECIMAL
from sqlalchemy.types import Enum as Enum
from sqlalchemy.types import FLOAT as FLOAT from sqlalchemy.types import FLOAT as FLOAT
from sqlalchemy.types import Float as Float
from sqlalchemy.types import INT as INT from sqlalchemy.types import INT as INT
from sqlalchemy.types import INTEGER as INTEGER from sqlalchemy.types import INTEGER as INTEGER
from sqlalchemy.types import Integer as Integer
from sqlalchemy.types import Interval as Interval
from sqlalchemy.types import JSON as JSON from sqlalchemy.types import JSON as JSON
from sqlalchemy.types import LargeBinary as LargeBinary
from sqlalchemy.types import NCHAR as NCHAR from sqlalchemy.types import NCHAR as NCHAR
from sqlalchemy.types import NUMERIC as NUMERIC from sqlalchemy.types import NUMERIC as NUMERIC
from sqlalchemy.types import Numeric as Numeric
from sqlalchemy.types import NVARCHAR as NVARCHAR from sqlalchemy.types import NVARCHAR as NVARCHAR
from sqlalchemy.types import PickleType as PickleType
from sqlalchemy.types import REAL as REAL from sqlalchemy.types import REAL as REAL
from sqlalchemy.types import SMALLINT as SMALLINT from sqlalchemy.types import SMALLINT as SMALLINT
from sqlalchemy.types import TEXT as TEXT
from sqlalchemy.types import TIME as TIME
from sqlalchemy.types import TIMESTAMP as TIMESTAMP
from sqlalchemy.types import VARBINARY as VARBINARY
from sqlalchemy.types import VARCHAR as VARCHAR
from sqlalchemy.types import BigInteger as BigInteger
from sqlalchemy.types import Boolean as Boolean
from sqlalchemy.types import Date as Date
from sqlalchemy.types import DateTime as DateTime
from sqlalchemy.types import Enum as Enum
from sqlalchemy.types import Float as Float
from sqlalchemy.types import Integer as Integer
from sqlalchemy.types import Interval as Interval
from sqlalchemy.types import LargeBinary as LargeBinary
from sqlalchemy.types import Numeric as Numeric
from sqlalchemy.types import PickleType as PickleType
from sqlalchemy.types import SmallInteger as SmallInteger from sqlalchemy.types import SmallInteger as SmallInteger
from sqlalchemy.types import String as String from sqlalchemy.types import String as String
from sqlalchemy.types import TEXT as TEXT
from sqlalchemy.types import Text as Text from sqlalchemy.types import Text as Text
from sqlalchemy.types import TIME as TIME
from sqlalchemy.types import Time as Time from sqlalchemy.types import Time as Time
from sqlalchemy.types import TIMESTAMP as TIMESTAMP
from sqlalchemy.types import TypeDecorator as TypeDecorator from sqlalchemy.types import TypeDecorator as TypeDecorator
from sqlalchemy.types import Unicode as Unicode from sqlalchemy.types import Unicode as Unicode
from sqlalchemy.types import UnicodeText as UnicodeText from sqlalchemy.types import UnicodeText as UnicodeText
from sqlalchemy.types import VARBINARY as VARBINARY
from sqlalchemy.types import VARCHAR as VARCHAR
# Extensions and modifications of SQLAlchemy in SQLModel # From SQLModel, modifications of SQLAlchemy or equivalents of Pydantic
from .engine.create import create_engine as create_engine from .engine.create import create_engine as create_engine
from .orm.session import Session as Session
from .sql.expression import select as select
from .sql.expression import col as col
from .sql.sqltypes import AutoString as AutoString
# Export SQLModel specifics (equivalent to Pydantic)
from .main import SQLModel as SQLModel
from .main import Field as Field from .main import Field as Field
from .main import Relationship as Relationship from .main import Relationship as Relationship
from .main import SQLModel as SQLModel
from .orm.session import Session as Session
from .sql.expression import col as col
from .sql.expression import select as select
from .sql.sqltypes import AutoString as AutoString

View File

@ -6,7 +6,7 @@ class _DefaultPlaceholder:
You shouldn't use this class directly. You shouldn't use this class directly.
It's used internally to recognize when a default value has been overwritten, even It's used internally to recognize when a default value has been overwritten, even
if the overriden default value was truthy. if the overridden default value was truthy.
""" """
def __init__(self, value: Any): def __init__(self, value: Any):
@ -27,6 +27,6 @@ def Default(value: _TDefaultType) -> _TDefaultType:
You shouldn't use this function directly. You shouldn't use this function directly.
It's used internally to recognize when a default value has been overwritten, even It's used internally to recognize when a default value has been overwritten, even
if the overriden default value was truthy. if the overridden default value was truthy.
""" """
return _DefaultPlaceholder(value) # type: ignore return _DefaultPlaceholder(value) # type: ignore

View File

@ -1,17 +1,17 @@
from typing import Any, Mapping, Optional, Sequence, TypeVar, Union from typing import Any, Mapping, Optional, Sequence, TypeVar, Union, overload
from sqlalchemy import util from sqlalchemy import util
from sqlalchemy.ext.asyncio import AsyncSession as _AsyncSession from sqlalchemy.ext.asyncio import AsyncSession as _AsyncSession
from sqlalchemy.ext.asyncio import engine from sqlalchemy.ext.asyncio import engine
from sqlalchemy.ext.asyncio.engine import AsyncConnection, AsyncEngine from sqlalchemy.ext.asyncio.engine import AsyncConnection, AsyncEngine
from sqlalchemy.util.concurrency import greenlet_spawn from sqlalchemy.util.concurrency import greenlet_spawn
from sqlmodel.sql.base import Executable
from ...engine.result import ScalarResult from ...engine.result import Result, ScalarResult
from ...orm.session import Session from ...orm.session import Session
from ...sql.expression import Select from ...sql.base import Executable
from ...sql.expression import Select, SelectOfScalar
_T = TypeVar("_T") _TSelectParam = TypeVar("_TSelectParam")
class AsyncSession(_AsyncSession): class AsyncSession(_AsyncSession):
@ -40,14 +40,46 @@ class AsyncSession(_AsyncSession):
Session(bind=bind, binds=binds, **kw) # type: ignore Session(bind=bind, binds=binds, **kw) # type: ignore
) )
@overload
async def exec( async def exec(
self, self,
statement: Union[Select[_T], Executable[_T]], statement: Select[_TSelectParam],
*,
params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None,
execution_options: Mapping[str, Any] = util.EMPTY_DICT,
bind_arguments: Optional[Mapping[str, Any]] = None,
_parent_execute_state: Optional[Any] = None,
_add_event: Optional[Any] = None,
**kw: Any,
) -> Result[_TSelectParam]:
...
@overload
async def exec(
self,
statement: SelectOfScalar[_TSelectParam],
*,
params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None,
execution_options: Mapping[str, Any] = util.EMPTY_DICT,
bind_arguments: Optional[Mapping[str, Any]] = None,
_parent_execute_state: Optional[Any] = None,
_add_event: Optional[Any] = None,
**kw: Any,
) -> ScalarResult[_TSelectParam]:
...
async def exec(
self,
statement: Union[
Select[_TSelectParam],
SelectOfScalar[_TSelectParam],
Executable[_TSelectParam],
],
params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None,
execution_options: Mapping[Any, Any] = util.EMPTY_DICT, execution_options: Mapping[Any, Any] = util.EMPTY_DICT,
bind_arguments: Optional[Mapping[str, Any]] = None, bind_arguments: Optional[Mapping[str, Any]] = None,
**kw: Any, **kw: Any,
) -> ScalarResult[_T]: ) -> Union[Result[_TSelectParam], ScalarResult[_TSelectParam]]:
# TODO: the documentation says execution_options accepts a dict, but only # TODO: the documentation says execution_options accepts a dict, but only
# util.immutabledict has the union() method. Is this a bug in SQLAlchemy? # util.immutabledict has the union() method. Is this a bug in SQLAlchemy?
execution_options = execution_options.union({"prebuffer_rows": True}) # type: ignore execution_options = execution_options.union({"prebuffer_rows": True}) # type: ignore

View File

@ -11,6 +11,7 @@ from typing import (
Callable, Callable,
ClassVar, ClassVar,
Dict, Dict,
ForwardRef,
List, List,
Mapping, Mapping,
Optional, Optional,
@ -21,19 +22,29 @@ from typing import (
TypeVar, TypeVar,
Union, Union,
cast, cast,
overload,
) )
from pydantic import BaseConfig, BaseModel from pydantic import BaseConfig, BaseModel
from pydantic.errors import ConfigError, DictError from pydantic.errors import ConfigError, DictError
from pydantic.fields import SHAPE_SINGLETON from pydantic.fields import SHAPE_SINGLETON, ModelField, Undefined, UndefinedType
from pydantic.fields import FieldInfo as PydanticFieldInfo from pydantic.fields import FieldInfo as PydanticFieldInfo
from pydantic.fields import ModelField, Undefined, UndefinedType
from pydantic.main import ModelMetaclass, validate_model from pydantic.main import ModelMetaclass, validate_model
from pydantic.typing import ForwardRef, NoArgAnyCallable, resolve_annotations from pydantic.typing import NoArgAnyCallable, resolve_annotations
from pydantic.utils import ROOT_KEY, Representation from pydantic.utils import ROOT_KEY, Representation
from sqlalchemy import Boolean, Column, Date, DateTime from sqlalchemy import (
Boolean,
Column,
Date,
DateTime,
Float,
ForeignKey,
Integer,
Interval,
Numeric,
inspect,
)
from sqlalchemy import Enum as sa_Enum from sqlalchemy import Enum as sa_Enum
from sqlalchemy import Float, ForeignKey, Integer, Interval, Numeric, inspect
from sqlalchemy.orm import RelationshipProperty, declared_attr, registry, relationship from sqlalchemy.orm import RelationshipProperty, declared_attr, registry, relationship
from sqlalchemy.orm.attributes import set_attribute from sqlalchemy.orm.attributes import set_attribute
from sqlalchemy.orm.decl_api import DeclarativeMeta from sqlalchemy.orm.decl_api import DeclarativeMeta
@ -78,6 +89,28 @@ class FieldInfo(PydanticFieldInfo):
"Passing sa_column_kwargs is not supported when " "Passing sa_column_kwargs is not supported when "
"also passing a sa_column" "also passing a sa_column"
) )
if primary_key is not Undefined:
raise RuntimeError(
"Passing primary_key is not supported when "
"also passing a sa_column"
)
if nullable is not Undefined:
raise RuntimeError(
"Passing nullable is not supported when " "also passing a sa_column"
)
if foreign_key is not Undefined:
raise RuntimeError(
"Passing foreign_key is not supported when "
"also passing a sa_column"
)
if unique is not Undefined:
raise RuntimeError(
"Passing unique is not supported when " "also passing a sa_column"
)
if index is not Undefined:
raise RuntimeError(
"Passing index is not supported when " "also passing a sa_column"
)
super().__init__(default=default, **kwargs) super().__init__(default=default, **kwargs)
self.primary_key = primary_key self.primary_key = primary_key
self.nullable = nullable self.nullable = nullable
@ -118,6 +151,7 @@ class RelationshipInfo(Representation):
self.sa_relationship_kwargs = sa_relationship_kwargs self.sa_relationship_kwargs = sa_relationship_kwargs
@overload
def Field( def Field(
default: Any = Undefined, default: Any = Undefined,
*, *,
@ -137,15 +171,99 @@ def Field(
lt: Optional[float] = None, lt: Optional[float] = None,
le: Optional[float] = None, le: Optional[float] = None,
multiple_of: Optional[float] = None, multiple_of: Optional[float] = None,
max_digits: Optional[int] = None,
decimal_places: Optional[int] = None,
min_items: Optional[int] = None, min_items: Optional[int] = None,
max_items: Optional[int] = None, max_items: Optional[int] = None,
unique_items: Optional[bool] = None,
min_length: Optional[int] = None, min_length: Optional[int] = None,
max_length: Optional[int] = None, max_length: Optional[int] = None,
allow_mutation: bool = True, allow_mutation: bool = True,
regex: Optional[str] = None, regex: Optional[str] = None,
primary_key: bool = False, discriminator: Optional[str] = None,
foreign_key: Optional[Any] = None, repr: bool = True,
unique: bool = False, primary_key: Union[bool, UndefinedType] = Undefined,
foreign_key: Any = Undefined,
unique: Union[bool, UndefinedType] = Undefined,
nullable: Union[bool, UndefinedType] = Undefined,
index: Union[bool, UndefinedType] = Undefined,
sa_column_args: Union[Sequence[Any], UndefinedType] = Undefined,
sa_column_kwargs: Union[Mapping[str, Any], UndefinedType] = Undefined,
schema_extra: Optional[Dict[str, Any]] = None,
) -> Any:
...
@overload
def Field(
default: Any = Undefined,
*,
default_factory: Optional[NoArgAnyCallable] = None,
alias: Optional[str] = None,
title: Optional[str] = None,
description: Optional[str] = None,
exclude: Union[
AbstractSet[Union[int, str]], Mapping[Union[int, str], Any], Any
] = None,
include: Union[
AbstractSet[Union[int, str]], Mapping[Union[int, str], Any], Any
] = None,
const: Optional[bool] = None,
gt: Optional[float] = None,
ge: Optional[float] = None,
lt: Optional[float] = None,
le: Optional[float] = None,
multiple_of: Optional[float] = None,
max_digits: Optional[int] = None,
decimal_places: Optional[int] = None,
min_items: Optional[int] = None,
max_items: Optional[int] = None,
unique_items: Optional[bool] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
allow_mutation: bool = True,
regex: Optional[str] = None,
discriminator: Optional[str] = None,
repr: bool = True,
sa_column: Union[Column, UndefinedType] = Undefined, # type: ignore
schema_extra: Optional[Dict[str, Any]] = None,
) -> Any:
...
def Field(
default: Any = Undefined,
*,
default_factory: Optional[NoArgAnyCallable] = None,
alias: Optional[str] = None,
title: Optional[str] = None,
description: Optional[str] = None,
exclude: Union[
AbstractSet[Union[int, str]], Mapping[Union[int, str], Any], Any
] = None,
include: Union[
AbstractSet[Union[int, str]], Mapping[Union[int, str], Any], Any
] = None,
const: Optional[bool] = None,
gt: Optional[float] = None,
ge: Optional[float] = None,
lt: Optional[float] = None,
le: Optional[float] = None,
multiple_of: Optional[float] = None,
max_digits: Optional[int] = None,
decimal_places: Optional[int] = None,
min_items: Optional[int] = None,
max_items: Optional[int] = None,
unique_items: Optional[bool] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
allow_mutation: bool = True,
regex: Optional[str] = None,
discriminator: Optional[str] = None,
repr: bool = True,
primary_key: Union[bool, UndefinedType] = Undefined,
foreign_key: Any = Undefined,
unique: Union[bool, UndefinedType] = Undefined,
nullable: Union[bool, UndefinedType] = Undefined, nullable: Union[bool, UndefinedType] = Undefined,
index: Union[bool, UndefinedType] = Undefined, index: Union[bool, UndefinedType] = Undefined,
sa_type: Type[Any] = Undefined, sa_type: Type[Any] = Undefined,
@ -169,12 +287,17 @@ def Field(
lt=lt, lt=lt,
le=le, le=le,
multiple_of=multiple_of, multiple_of=multiple_of,
max_digits=max_digits,
decimal_places=decimal_places,
min_items=min_items, min_items=min_items,
max_items=max_items, max_items=max_items,
unique_items=unique_items,
min_length=min_length, min_length=min_length,
max_length=max_length, max_length=max_length,
allow_mutation=allow_mutation, allow_mutation=allow_mutation,
regex=regex, regex=regex,
discriminator=discriminator,
repr=repr,
primary_key=primary_key, primary_key=primary_key,
foreign_key=foreign_key, foreign_key=foreign_key,
unique=unique, unique=unique,
@ -190,6 +313,27 @@ def Field(
return field_info return field_info
@overload
def Relationship(
*,
back_populates: Optional[str] = None,
link_model: Optional[Any] = None,
sa_relationship_args: Optional[Sequence[Any]] = None,
sa_relationship_kwargs: Optional[Mapping[str, Any]] = None,
) -> Any:
...
@overload
def Relationship(
*,
back_populates: Optional[str] = None,
link_model: Optional[Any] = None,
sa_relationship: Optional[RelationshipProperty] = None, # type: ignore
) -> Any:
...
def Relationship( def Relationship(
*, *,
back_populates: Optional[str] = None, back_populates: Optional[str] = None,
@ -308,9 +452,9 @@ class SQLModelMetaclass(ModelMetaclass, DeclarativeMeta):
config_registry = cast(registry, config_registry) config_registry = cast(registry, config_registry)
# If it was passed by kwargs, ensure it's also set in config # If it was passed by kwargs, ensure it's also set in config
new_cls.__config__.registry = config_table new_cls.__config__.registry = config_table
setattr(new_cls, "_sa_registry", config_registry) setattr(new_cls, "_sa_registry", config_registry) # noqa: B010
setattr(new_cls, "metadata", config_registry.metadata) setattr(new_cls, "metadata", config_registry.metadata) # noqa: B010
setattr(new_cls, "__abstract__", True) setattr(new_cls, "__abstract__", True) # noqa: B010
return new_cls return new_cls
# Override SQLAlchemy, allow both SQLAlchemy and plain Pydantic models # Override SQLAlchemy, allow both SQLAlchemy and plain Pydantic models
@ -323,19 +467,16 @@ class SQLModelMetaclass(ModelMetaclass, DeclarativeMeta):
# triggers an error # triggers an error
base_is_table = False base_is_table = False
for base in bases: for base in bases:
config = getattr(base, "__config__") config = getattr(base, "__config__") # noqa: B009
if config and getattr(config, "table", False): if config and getattr(config, "table", False):
base_is_table = True base_is_table = True
break break
if getattr(cls.__config__, "table", False) and not base_is_table: if getattr(cls.__config__, "table", False) and not base_is_table:
dict_used = dict_.copy()
for field_name, field_value in cls.__fields__.items():
dict_used[field_name] = get_column_from_field(field_value)
for rel_name, rel_info in cls.__sqlmodel_relationships__.items(): for rel_name, rel_info in cls.__sqlmodel_relationships__.items():
if rel_info.sa_relationship: if rel_info.sa_relationship:
# There's a SQLAlchemy relationship declared, that takes precedence # There's a SQLAlchemy relationship declared, that takes precedence
# over anything else, use that and continue with the next attribute # over anything else, use that and continue with the next attribute
dict_used[rel_name] = rel_info.sa_relationship setattr(cls, rel_name, rel_info.sa_relationship) # Fix #315
continue continue
ann = cls.__annotations__[rel_name] ann = cls.__annotations__[rel_name]
temp_field = ModelField.infer( temp_field = ModelField.infer(
@ -353,7 +494,7 @@ class SQLModelMetaclass(ModelMetaclass, DeclarativeMeta):
rel_kwargs["back_populates"] = rel_info.back_populates rel_kwargs["back_populates"] = rel_info.back_populates
if rel_info.link_model: if rel_info.link_model:
ins = inspect(rel_info.link_model) ins = inspect(rel_info.link_model)
local_table = getattr(ins, "local_table") local_table = getattr(ins, "local_table") # noqa: B009
if local_table is None: if local_table is None:
raise RuntimeError( raise RuntimeError(
"Couldn't find the secondary table for " "Couldn't find the secondary table for "
@ -368,9 +509,11 @@ class SQLModelMetaclass(ModelMetaclass, DeclarativeMeta):
rel_value: RelationshipProperty = relationship( # type: ignore rel_value: RelationshipProperty = relationship( # type: ignore
relationship_to, *rel_args, **rel_kwargs relationship_to, *rel_args, **rel_kwargs
) )
dict_used[rel_name] = rel_value
setattr(cls, rel_name, rel_value) # Fix #315 setattr(cls, rel_name, rel_value) # Fix #315
DeclarativeMeta.__init__(cls, classname, bases, dict_used, **kw) # SQLAlchemy no longer uses dict_
# Ref: https://github.com/sqlalchemy/sqlalchemy/commit/428ea01f00a9cc7f85e435018565eb6da7af1b77
# Tag: 1.4.36
DeclarativeMeta.__init__(cls, classname, bases, dict_, **kw)
else: else:
ModelMetaclass.__init__(cls, classname, bases, dict_, **kw) ModelMetaclass.__init__(cls, classname, bases, dict_, **kw)
@ -379,6 +522,10 @@ def get_sqlalchemy_type(field: ModelField) -> Any:
if hasattr(field.field_info, "sa_type"): if hasattr(field.field_info, "sa_type"):
if not issubclass(type(field.field_info.sa_type), type(Undefined)): if not issubclass(type(field.field_info.sa_type), type(Undefined)):
return field.field_info.sa_type return field.field_info.sa_type
if isinstance(field.type_, type) and field.shape == SHAPE_SINGLETON:
# Check enums first as an enum can also be a str, needed by Pydantic/FastAPI
if issubclass(field.type_, Enum):
return sa_Enum(field.type_)
if issubclass(field.type_, str): if issubclass(field.type_, str):
if field.field_info.max_length: if field.field_info.max_length:
return AutoString(length=field.field_info.max_length) return AutoString(length=field.field_info.max_length)
@ -397,8 +544,6 @@ def get_sqlalchemy_type(field: ModelField) -> Any:
return Interval return Interval
if issubclass(field.type_, time): if issubclass(field.type_, time):
return Time return Time
if issubclass(field.type_, Enum):
return sa_Enum(field.type_)
if issubclass(field.type_, bytes): if issubclass(field.type_, bytes):
return LargeBinary return LargeBinary
if issubclass(field.type_, Decimal): if issubclass(field.type_, Decimal):
@ -426,21 +571,28 @@ def get_column_from_field(field: ModelField) -> Column: # type: ignore
if isinstance(sa_column, Column): if isinstance(sa_column, Column):
return sa_column return sa_column
sa_type = get_sqlalchemy_type(field) sa_type = get_sqlalchemy_type(field)
primary_key = getattr(field.field_info, "primary_key", False) primary_key = getattr(field.field_info, "primary_key", Undefined)
if primary_key is Undefined:
primary_key = False
index = getattr(field.field_info, "index", Undefined) index = getattr(field.field_info, "index", Undefined)
if index is Undefined: if index is Undefined:
index = False index = False
nullable = not primary_key and _is_field_noneable(field) nullable = not primary_key and _is_field_noneable(field)
# Override derived nullability if the nullable property is set explicitly # Override derived nullability if the nullable property is set explicitly
# on the field # on the field
if hasattr(field.field_info, "nullable"): field_nullable = getattr(field.field_info, "nullable", Undefined) # noqa: B009
field_nullable = getattr(field.field_info, "nullable")
if field_nullable != Undefined: if field_nullable != Undefined:
assert not isinstance(field_nullable, UndefinedType)
nullable = field_nullable nullable = field_nullable
args = [] args = []
foreign_key = getattr(field.field_info, "foreign_key", None) foreign_key = getattr(field.field_info, "foreign_key", Undefined)
unique = getattr(field.field_info, "unique", False) if foreign_key is Undefined:
foreign_key = None
unique = getattr(field.field_info, "unique", Undefined)
if unique is Undefined:
unique = False
if foreign_key: if foreign_key:
assert isinstance(foreign_key, str)
args.append(ForeignKey(foreign_key)) args.append(ForeignKey(foreign_key))
kwargs = { kwargs = {
"primary_key": primary_key, "primary_key": primary_key,
@ -583,7 +735,11 @@ class SQLModel(BaseModel, metaclass=SQLModelMetaclass, registry=default_registry
def __repr_args__(self) -> Sequence[Tuple[Optional[str], Any]]: def __repr_args__(self) -> Sequence[Tuple[Optional[str], Any]]:
# Don't show SQLAlchemy private attributes # Don't show SQLAlchemy private attributes
return [(k, v) for k, v in self.__dict__.items() if not k.startswith("_sa_")] return [
(k, v)
for k, v in super().__repr_args__()
if not (isinstance(k, str) and k.startswith("_sa_"))
]
# From Pydantic, override to enforce validation with dict # From Pydantic, override to enforce validation with dict
@classmethod @classmethod

View File

@ -4,11 +4,11 @@ from sqlalchemy import util
from sqlalchemy.orm import Query as _Query from sqlalchemy.orm import Query as _Query
from sqlalchemy.orm import Session as _Session from sqlalchemy.orm import Session as _Session
from sqlalchemy.sql.base import Executable as _Executable from sqlalchemy.sql.base import Executable as _Executable
from sqlmodel.sql.expression import Select, SelectOfScalar
from typing_extensions import Literal from typing_extensions import Literal
from ..engine.result import Result, ScalarResult from ..engine.result import Result, ScalarResult
from ..sql.base import Executable from ..sql.base import Executable
from ..sql.expression import Select, SelectOfScalar
_TSelectParam = TypeVar("_TSelectParam") _TSelectParam = TypeVar("_TSelectParam")

View File

@ -1,6 +1,5 @@
# WARNING: do not modify this code, it is generated by expression.py.jinja2 # WARNING: do not modify this code, it is generated by expression.py.jinja2
import sys
from datetime import datetime from datetime import datetime
from typing import ( from typing import (
TYPE_CHECKING, TYPE_CHECKING,
@ -12,7 +11,6 @@ from typing import (
Type, Type,
TypeVar, TypeVar,
Union, Union,
cast,
overload, overload,
) )
from uuid import UUID from uuid import UUID
@ -24,13 +22,11 @@ from sqlalchemy.sql.expression import Select as _Select
_TSelect = TypeVar("_TSelect") _TSelect = TypeVar("_TSelect")
# Workaround Generics incompatibility in Python 3.6
# Ref: https://github.com/python/typing/issues/449#issuecomment-316061322
if sys.version_info.minor >= 7:
class Select(_Select, Generic[_TSelect]): class Select(_Select, Generic[_TSelect]):
inherit_cache = True inherit_cache = True
# This is not comparable to sqlalchemy.sql.selectable.ScalarSelect, that has a different # This is not comparable to sqlalchemy.sql.selectable.ScalarSelect, that has a different
# purpose. This is the same as a normal SQLAlchemy Select class where there's only one # purpose. This is the same as a normal SQLAlchemy Select class where there's only one
# entity, so the result will be converted to a scalar by default. This way writing # entity, so the result will be converted to a scalar by default. This way writing
@ -38,23 +34,6 @@ if sys.version_info.minor >= 7:
class SelectOfScalar(_Select, Generic[_TSelect]): class SelectOfScalar(_Select, Generic[_TSelect]):
inherit_cache = True inherit_cache = True
else:
from typing import GenericMeta # type: ignore
class GenericSelectMeta(GenericMeta, _Select.__class__): # type: ignore
pass
class _Py36Select(_Select, Generic[_TSelect], metaclass=GenericSelectMeta):
inherit_cache = True
class _Py36SelectOfScalar(_Select, Generic[_TSelect], metaclass=GenericSelectMeta):
inherit_cache = True
# Cast them for editors to work correctly, from several tricks tried, this works
# for both VS Code and PyCharm
Select = cast("Select", _Py36Select) # type: ignore
SelectOfScalar = cast("SelectOfScalar", _Py36SelectOfScalar) # type: ignore
if TYPE_CHECKING: # pragma: no cover if TYPE_CHECKING: # pragma: no cover
from ..main import SQLModel from ..main import SQLModel

View File

@ -1,4 +1,3 @@
import sys
from datetime import datetime from datetime import datetime
from typing import ( from typing import (
TYPE_CHECKING, TYPE_CHECKING,
@ -10,7 +9,6 @@ from typing import (
Type, Type,
TypeVar, TypeVar,
Union, Union,
cast,
overload, overload,
) )
from uuid import UUID from uuid import UUID
@ -22,10 +20,6 @@ from sqlalchemy.sql.expression import Select as _Select
_TSelect = TypeVar("_TSelect") _TSelect = TypeVar("_TSelect")
# Workaround Generics incompatibility in Python 3.6
# Ref: https://github.com/python/typing/issues/449#issuecomment-316061322
if sys.version_info.minor >= 7:
class Select(_Select, Generic[_TSelect]): class Select(_Select, Generic[_TSelect]):
inherit_cache = True inherit_cache = True
@ -36,24 +30,6 @@ if sys.version_info.minor >= 7:
class SelectOfScalar(_Select, Generic[_TSelect]): class SelectOfScalar(_Select, Generic[_TSelect]):
inherit_cache = True inherit_cache = True
else:
from typing import GenericMeta # type: ignore
class GenericSelectMeta(GenericMeta, _Select.__class__): # type: ignore
pass
class _Py36Select(_Select, Generic[_TSelect], metaclass=GenericSelectMeta):
inherit_cache = True
class _Py36SelectOfScalar(_Select, Generic[_TSelect], metaclass=GenericSelectMeta):
inherit_cache = True
# Cast them for editors to work correctly, from several tricks tried, this works
# for both VS Code and PyCharm
Select = cast("Select", _Py36Select) # type: ignore
SelectOfScalar = cast("SelectOfScalar", _Py36SelectOfScalar) # type: ignore
if TYPE_CHECKING: # pragma: no cover if TYPE_CHECKING: # pragma: no cover
from ..main import SQLModel from ..main import SQLModel

View File

@ -8,7 +8,6 @@ from sqlalchemy.sql.type_api import TypeEngine
class AutoString(types.TypeDecorator): # type: ignore class AutoString(types.TypeDecorator): # type: ignore
impl = types.String impl = types.String
cache_ok = True cache_ok = True
mysql_default_length = 255 mysql_default_length = 255

View File

@ -42,8 +42,7 @@ def coverage_run(*, module: str, cwd: Union[str, Path]) -> subprocess.CompletedP
module, module,
], ],
cwd=str(cwd), cwd=str(cwd),
stdout=subprocess.PIPE, capture_output=True,
stderr=subprocess.PIPE,
encoding="utf-8", encoding="utf-8",
) )
return result return result

View File

@ -14,12 +14,12 @@ Associated issues:
""" """
class MyEnum1(enum.Enum): class MyEnum1(str, enum.Enum):
A = "A" A = "A"
B = "B" B = "B"
class MyEnum2(enum.Enum): class MyEnum2(str, enum.Enum):
C = "C" C = "C"
D = "D" D = "D"
@ -70,3 +70,43 @@ def test_sqlite_ddl_sql(capsys):
captured = capsys.readouterr() captured = capsys.readouterr()
assert "enum_field VARCHAR(1) NOT NULL" in captured.out assert "enum_field VARCHAR(1) NOT NULL" in captured.out
assert "CREATE TYPE" not in captured.out assert "CREATE TYPE" not in captured.out
def test_json_schema_flat_model():
assert FlatModel.schema() == {
"title": "FlatModel",
"type": "object",
"properties": {
"id": {"title": "Id", "type": "string", "format": "uuid"},
"enum_field": {"$ref": "#/definitions/MyEnum1"},
},
"required": ["id", "enum_field"],
"definitions": {
"MyEnum1": {
"title": "MyEnum1",
"description": "An enumeration.",
"enum": ["A", "B"],
"type": "string",
}
},
}
def test_json_schema_inherit_model():
assert InheritModel.schema() == {
"title": "InheritModel",
"type": "object",
"properties": {
"id": {"title": "Id", "type": "string", "format": "uuid"},
"enum_field": {"$ref": "#/definitions/MyEnum2"},
},
"required": ["id", "enum_field"],
"definitions": {
"MyEnum2": {
"title": "MyEnum2",
"description": "An enumeration.",
"enum": ["C", "D"],
"type": "string",
}
},
}

View File

@ -0,0 +1,39 @@
from typing import Optional
from sqlalchemy import ForeignKey
from sqlmodel import Field, SQLModel, create_engine
def test_sa_column_args(clear_sqlmodel, caplog) -> None:
class Team(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
team_id: Optional[int] = Field(
default=None,
sa_column_args=[ForeignKey("team.id")],
)
engine = create_engine("sqlite://", echo=True)
SQLModel.metadata.create_all(engine)
create_table_log = [
message for message in caplog.messages if "CREATE TABLE hero" in message
][0]
assert "FOREIGN KEY(team_id) REFERENCES team (id)" in create_table_log
def test_sa_column_kargs(clear_sqlmodel, caplog) -> None:
class Item(SQLModel, table=True):
id: Optional[int] = Field(
default=None,
sa_column_kwargs={"primary_key": True},
)
engine = create_engine("sqlite://", echo=True)
SQLModel.metadata.create_all(engine)
create_table_log = [
message for message in caplog.messages if "CREATE TABLE item" in message
][0]
assert "PRIMARY KEY (id)" in create_table_log

View File

@ -0,0 +1,99 @@
from typing import Optional
import pytest
from sqlalchemy import Column, Integer, String
from sqlmodel import Field, SQLModel
def test_sa_column_takes_precedence() -> None:
class Item(SQLModel, table=True):
id: Optional[int] = Field(
default=None,
sa_column=Column(String, primary_key=True, nullable=False),
)
# It would have been nullable with no sa_column
assert Item.id.nullable is False # type: ignore
assert isinstance(Item.id.type, String) # type: ignore
def test_sa_column_no_sa_args() -> None:
with pytest.raises(RuntimeError):
class Item(SQLModel, table=True):
id: Optional[int] = Field(
default=None,
sa_column_args=[Integer],
sa_column=Column(Integer, primary_key=True),
)
def test_sa_column_no_sa_kargs() -> None:
with pytest.raises(RuntimeError):
class Item(SQLModel, table=True):
id: Optional[int] = Field(
default=None,
sa_column_kwargs={"primary_key": True},
sa_column=Column(Integer, primary_key=True),
)
def test_sa_column_no_primary_key() -> None:
with pytest.raises(RuntimeError):
class Item(SQLModel, table=True):
id: Optional[int] = Field(
default=None,
primary_key=True,
sa_column=Column(Integer, primary_key=True),
)
def test_sa_column_no_nullable() -> None:
with pytest.raises(RuntimeError):
class Item(SQLModel, table=True):
id: Optional[int] = Field(
default=None,
nullable=True,
sa_column=Column(Integer, primary_key=True),
)
def test_sa_column_no_foreign_key() -> None:
with pytest.raises(RuntimeError):
class Team(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
team_id: Optional[int] = Field(
default=None,
foreign_key="team.id",
sa_column=Column(Integer, primary_key=True),
)
def test_sa_column_no_unique() -> None:
with pytest.raises(RuntimeError):
class Item(SQLModel, table=True):
id: Optional[int] = Field(
default=None,
unique=True,
sa_column=Column(Integer, primary_key=True),
)
def test_sa_column_no_index() -> None:
with pytest.raises(RuntimeError):
class Item(SQLModel, table=True):
id: Optional[int] = Field(
default=None,
index=True,
sa_column=Column(Integer, primary_key=True),
)

View File

@ -0,0 +1,53 @@
from typing import List, Optional
import pytest
from sqlalchemy.orm import relationship
from sqlmodel import Field, Relationship, SQLModel
def test_sa_relationship_no_args() -> None:
with pytest.raises(RuntimeError):
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",
sa_relationship_args=["Hero"],
sa_relationship=relationship("Hero", 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")
team: Optional[Team] = Relationship(back_populates="heroes")
def test_sa_relationship_no_kwargs() -> None:
with pytest.raises(RuntimeError):
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",
sa_relationship_kwargs={"lazy": "selectin"},
sa_relationship=relationship("Hero", 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")
team: Optional[Team] = Relationship(back_populates="heroes")

View File

@ -1,8 +1,9 @@
from typing import Optional from typing import List, Optional
import pytest import pytest
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from sqlmodel import Field, Session, SQLModel, create_engine from sqlalchemy.orm import RelationshipProperty
from sqlmodel import Field, Relationship, Session, SQLModel, create_engine
def test_should_allow_duplicate_row_if_unique_constraint_is_not_passed(clear_sqlmodel): def test_should_allow_duplicate_row_if_unique_constraint_is_not_passed(clear_sqlmodel):
@ -91,3 +92,37 @@ def test_should_raise_exception_when_try_to_duplicate_row_if_unique_constraint_i
session.add(hero_2) session.add(hero_2)
session.commit() session.commit()
session.refresh(hero_2) session.refresh(hero_2)
def test_sa_relationship_property(clear_sqlmodel):
"""Test https://github.com/tiangolo/sqlmodel/issues/315#issuecomment-1272122306"""
class Team(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(unique=True)
heroes: List["Hero"] = Relationship( # noqa: F821
sa_relationship=RelationshipProperty("Hero", back_populates="team")
)
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(unique=True)
team_id: Optional[int] = Field(default=None, foreign_key="team.id")
team: Optional[Team] = Relationship(
sa_relationship=RelationshipProperty("Team", back_populates="heroes")
)
team_preventers = Team(name="Preventers")
hero_rusty_man = Hero(name="Rusty-Man", team=team_preventers)
engine = create_engine("sqlite://", echo=True)
SQLModel.metadata.create_all(engine)
with Session(engine) as session:
session.add(hero_rusty_man)
session.commit()
session.refresh(hero_rusty_man)
# The next statement should not raise an AttributeError
assert hero_rusty_man.team
assert hero_rusty_man.team.name == "Preventers"

View File

View File

@ -0,0 +1,57 @@
from decimal import Decimal
from typing import Optional, Union
import pytest
from pydantic import ValidationError
from sqlmodel import Field, SQLModel
from typing_extensions import Literal
def test_decimal():
class Model(SQLModel):
dec: Decimal = Field(max_digits=4, decimal_places=2)
Model(dec=Decimal("3.14"))
Model(dec=Decimal("69.42"))
with pytest.raises(ValidationError):
Model(dec=Decimal("3.142"))
with pytest.raises(ValidationError):
Model(dec=Decimal("0.069"))
with pytest.raises(ValidationError):
Model(dec=Decimal("420"))
def test_discriminator():
# Example adapted from
# [Pydantic docs](https://pydantic-docs.helpmanual.io/usage/types/#discriminated-unions-aka-tagged-unions):
class Cat(SQLModel):
pet_type: Literal["cat"]
meows: int
class Dog(SQLModel):
pet_type: Literal["dog"]
barks: float
class Lizard(SQLModel):
pet_type: Literal["reptile", "lizard"]
scales: bool
class Model(SQLModel):
pet: Union[Cat, Dog, Lizard] = Field(..., discriminator="pet_type")
n: int
Model(pet={"pet_type": "dog", "barks": 3.14}, n=1) # type: ignore[arg-type]
with pytest.raises(ValidationError):
Model(pet={"pet_type": "dog"}, n=1) # type: ignore[arg-type]
def test_repr():
class Model(SQLModel):
id: Optional[int] = Field(primary_key=True)
foo: str = Field(repr=False)
instance = Model(id=123, foo="bar")
assert "foo=" not in repr(instance)

View File

@ -0,0 +1,28 @@
from typing import Any, Dict, List, Optional, Union
import pytest
from sqlmodel import Field, SQLModel
def test_type_list_breaks() -> None:
with pytest.raises(ValueError):
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
tags: List[str]
def test_type_dict_breaks() -> None:
with pytest.raises(ValueError):
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
tags: Dict[str, Any]
def test_type_union_breaks() -> None:
with pytest.raises(ValueError):
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
tags: Union[int, str]

View File

@ -2,7 +2,63 @@ from fastapi.testclient import TestClient
from sqlmodel import create_engine from sqlmodel import create_engine
from sqlmodel.pool import StaticPool from sqlmodel.pool import StaticPool
openapi_schema = {
def test_tutorial(clear_sqlmodel):
from docs_src.tutorial.fastapi.delete import tutorial001 as mod
mod.sqlite_url = "sqlite://"
mod.engine = create_engine(
mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool
)
with TestClient(mod.app) as client:
hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"}
hero2_data = {
"name": "Spider-Boy",
"secret_name": "Pedro Parqueador",
"id": 9000,
}
hero3_data = {
"name": "Rusty-Man",
"secret_name": "Tommy Sharp",
"age": 48,
}
response = client.post("/heroes/", json=hero1_data)
assert response.status_code == 200, response.text
response = client.post("/heroes/", json=hero2_data)
assert response.status_code == 200, response.text
hero2 = response.json()
hero2_id = hero2["id"]
response = client.post("/heroes/", json=hero3_data)
assert response.status_code == 200, response.text
response = client.get(f"/heroes/{hero2_id}")
assert response.status_code == 200, response.text
response = client.get("/heroes/9000")
assert response.status_code == 404, response.text
response = client.get("/heroes/")
assert response.status_code == 200, response.text
data = response.json()
assert len(data) == 3
response = client.patch(
f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"}
)
assert response.status_code == 200, response.text
response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"})
assert response.status_code == 404, response.text
response = client.delete(f"/heroes/{hero2_id}")
assert response.status_code == 200, response.text
response = client.get("/heroes/")
assert response.status_code == 200, response.text
data = response.json()
assert len(data) == 2
response = client.delete("/heroes/9000")
assert response.status_code == 404, response.text
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
"openapi": "3.0.2", "openapi": "3.0.2",
"info": {"title": "FastAPI", "version": "0.1.0"}, "info": {"title": "FastAPI", "version": "0.1.0"},
"paths": { "paths": {
@ -13,7 +69,11 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": False, "required": False,
"schema": {"title": "Offset", "type": "integer", "default": 0}, "schema": {
"title": "Offset",
"type": "integer",
"default": 0,
},
"name": "offset", "name": "offset",
"in": "query", "in": "query",
}, },
@ -21,9 +81,9 @@ openapi_schema = {
"required": False, "required": False,
"schema": { "schema": {
"title": "Limit", "title": "Limit",
"maximum": 100.0,
"type": "integer", "type": "integer",
"default": 100, "default": 100,
"lte": 100,
}, },
"name": "limit", "name": "limit",
"in": "query", "in": "query",
@ -37,7 +97,9 @@ openapi_schema = {
"schema": { "schema": {
"title": "Response Read Heroes Heroes Get", "title": "Response Read Heroes Heroes Get",
"type": "array", "type": "array",
"items": {"$ref": "#/components/schemas/HeroRead"}, "items": {
"$ref": "#/components/schemas/HeroRead"
},
} }
} }
}, },
@ -60,7 +122,9 @@ openapi_schema = {
"requestBody": { "requestBody": {
"content": { "content": {
"application/json": { "application/json": {
"schema": {"$ref": "#/components/schemas/HeroCreate"} "schema": {
"$ref": "#/components/schemas/HeroCreate"
}
} }
}, },
"required": True, "required": True,
@ -70,7 +134,9 @@ openapi_schema = {
"description": "Successful Response", "description": "Successful Response",
"content": { "content": {
"application/json": { "application/json": {
"schema": {"$ref": "#/components/schemas/HeroRead"} "schema": {
"$ref": "#/components/schemas/HeroRead"
}
} }
}, },
}, },
@ -104,7 +170,9 @@ openapi_schema = {
"description": "Successful Response", "description": "Successful Response",
"content": { "content": {
"application/json": { "application/json": {
"schema": {"$ref": "#/components/schemas/HeroRead"} "schema": {
"$ref": "#/components/schemas/HeroRead"
}
} }
}, },
}, },
@ -162,7 +230,9 @@ openapi_schema = {
"requestBody": { "requestBody": {
"content": { "content": {
"application/json": { "application/json": {
"schema": {"$ref": "#/components/schemas/HeroUpdate"} "schema": {
"$ref": "#/components/schemas/HeroUpdate"
}
} }
}, },
"required": True, "required": True,
@ -172,7 +242,9 @@ openapi_schema = {
"description": "Successful Response", "description": "Successful Response",
"content": { "content": {
"application/json": { "application/json": {
"schema": {"$ref": "#/components/schemas/HeroRead"} "schema": {
"$ref": "#/components/schemas/HeroRead"
}
} }
}, },
}, },
@ -199,7 +271,9 @@ openapi_schema = {
"detail": { "detail": {
"title": "Detail", "title": "Detail",
"type": "array", "type": "array",
"items": {"$ref": "#/components/schemas/ValidationError"}, "items": {
"$ref": "#/components/schemas/ValidationError"
},
} }
}, },
}, },
@ -250,62 +324,3 @@ openapi_schema = {
} }
}, },
} }
def test_tutorial(clear_sqlmodel):
from docs_src.tutorial.fastapi.delete import tutorial001 as mod
mod.sqlite_url = "sqlite://"
mod.engine = create_engine(
mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool
)
with TestClient(mod.app) as client:
hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"}
hero2_data = {
"name": "Spider-Boy",
"secret_name": "Pedro Parqueador",
"id": 9000,
}
hero3_data = {
"name": "Rusty-Man",
"secret_name": "Tommy Sharp",
"age": 48,
}
response = client.post("/heroes/", json=hero1_data)
assert response.status_code == 200, response.text
response = client.post("/heroes/", json=hero2_data)
assert response.status_code == 200, response.text
hero2 = response.json()
hero2_id = hero2["id"]
response = client.post("/heroes/", json=hero3_data)
assert response.status_code == 200, response.text
response = client.get(f"/heroes/{hero2_id}")
assert response.status_code == 200, response.text
response = client.get("/heroes/9000")
assert response.status_code == 404, response.text
response = client.get("/openapi.json")
data = response.json()
assert response.status_code == 200, response.text
assert data == openapi_schema
response = client.get("/heroes/")
assert response.status_code == 200, response.text
data = response.json()
assert len(data) == 3
response = client.patch(
f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"}
)
assert response.status_code == 200, response.text
response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"})
assert response.status_code == 404, response.text
response = client.delete(f"/heroes/{hero2_id}")
assert response.status_code == 200, response.text
response = client.get("/heroes/")
assert response.status_code == 200, response.text
data = response.json()
assert len(data) == 2
response = client.delete("/heroes/9000")
assert response.status_code == 404, response.text

View File

@ -2,7 +2,68 @@ from fastapi.testclient import TestClient
from sqlmodel import create_engine from sqlmodel import create_engine
from sqlmodel.pool import StaticPool from sqlmodel.pool import StaticPool
openapi_schema = {
def test_tutorial(clear_sqlmodel):
from docs_src.tutorial.fastapi.limit_and_offset import tutorial001 as mod
mod.sqlite_url = "sqlite://"
mod.engine = create_engine(
mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool
)
with TestClient(mod.app) as client:
hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"}
hero2_data = {
"name": "Spider-Boy",
"secret_name": "Pedro Parqueador",
"id": 9000,
}
hero3_data = {
"name": "Rusty-Man",
"secret_name": "Tommy Sharp",
"age": 48,
}
response = client.post("/heroes/", json=hero1_data)
assert response.status_code == 200, response.text
response = client.post("/heroes/", json=hero2_data)
assert response.status_code == 200, response.text
hero2 = response.json()
hero_id = hero2["id"]
response = client.post("/heroes/", json=hero3_data)
assert response.status_code == 200, response.text
response = client.get(f"/heroes/{hero_id}")
assert response.status_code == 200, response.text
response = client.get("/heroes/9000")
assert response.status_code == 404, response.text
response = client.get("/heroes/")
assert response.status_code == 200, response.text
data = response.json()
assert len(data) == 3
response = client.get("/heroes/", params={"limit": 2})
assert response.status_code == 200, response.text
data = response.json()
assert len(data) == 2
assert data[0]["name"] == hero1_data["name"]
assert data[1]["name"] == hero2_data["name"]
response = client.get("/heroes/", params={"offset": 1})
assert response.status_code == 200, response.text
data = response.json()
assert len(data) == 2
assert data[0]["name"] == hero2_data["name"]
assert data[1]["name"] == hero3_data["name"]
response = client.get("/heroes/", params={"offset": 1, "limit": 1})
assert response.status_code == 200, response.text
data = response.json()
assert len(data) == 1
assert data[0]["name"] == hero2_data["name"]
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
"openapi": "3.0.2", "openapi": "3.0.2",
"info": {"title": "FastAPI", "version": "0.1.0"}, "info": {"title": "FastAPI", "version": "0.1.0"},
"paths": { "paths": {
@ -13,7 +74,11 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": False, "required": False,
"schema": {"title": "Offset", "type": "integer", "default": 0}, "schema": {
"title": "Offset",
"type": "integer",
"default": 0,
},
"name": "offset", "name": "offset",
"in": "query", "in": "query",
}, },
@ -21,9 +86,9 @@ openapi_schema = {
"required": False, "required": False,
"schema": { "schema": {
"title": "Limit", "title": "Limit",
"maximum": 100.0,
"type": "integer", "type": "integer",
"default": 100, "default": 100,
"lte": 100,
}, },
"name": "limit", "name": "limit",
"in": "query", "in": "query",
@ -37,7 +102,9 @@ openapi_schema = {
"schema": { "schema": {
"title": "Response Read Heroes Heroes Get", "title": "Response Read Heroes Heroes Get",
"type": "array", "type": "array",
"items": {"$ref": "#/components/schemas/HeroRead"}, "items": {
"$ref": "#/components/schemas/HeroRead"
},
} }
} }
}, },
@ -60,7 +127,9 @@ openapi_schema = {
"requestBody": { "requestBody": {
"content": { "content": {
"application/json": { "application/json": {
"schema": {"$ref": "#/components/schemas/HeroCreate"} "schema": {
"$ref": "#/components/schemas/HeroCreate"
}
} }
}, },
"required": True, "required": True,
@ -70,7 +139,9 @@ openapi_schema = {
"description": "Successful Response", "description": "Successful Response",
"content": { "content": {
"application/json": { "application/json": {
"schema": {"$ref": "#/components/schemas/HeroRead"} "schema": {
"$ref": "#/components/schemas/HeroRead"
}
} }
}, },
}, },
@ -104,7 +175,9 @@ openapi_schema = {
"description": "Successful Response", "description": "Successful Response",
"content": { "content": {
"application/json": { "application/json": {
"schema": {"$ref": "#/components/schemas/HeroRead"} "schema": {
"$ref": "#/components/schemas/HeroRead"
}
} }
}, },
}, },
@ -131,7 +204,9 @@ openapi_schema = {
"detail": { "detail": {
"title": "Detail", "title": "Detail",
"type": "array", "type": "array",
"items": {"$ref": "#/components/schemas/ValidationError"}, "items": {
"$ref": "#/components/schemas/ValidationError"
},
} }
}, },
}, },
@ -173,67 +248,3 @@ openapi_schema = {
} }
}, },
} }
def test_tutorial(clear_sqlmodel):
from docs_src.tutorial.fastapi.limit_and_offset import tutorial001 as mod
mod.sqlite_url = "sqlite://"
mod.engine = create_engine(
mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool
)
with TestClient(mod.app) as client:
hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"}
hero2_data = {
"name": "Spider-Boy",
"secret_name": "Pedro Parqueador",
"id": 9000,
}
hero3_data = {
"name": "Rusty-Man",
"secret_name": "Tommy Sharp",
"age": 48,
}
response = client.post("/heroes/", json=hero1_data)
assert response.status_code == 200, response.text
response = client.post("/heroes/", json=hero2_data)
assert response.status_code == 200, response.text
hero2 = response.json()
hero_id = hero2["id"]
response = client.post("/heroes/", json=hero3_data)
assert response.status_code == 200, response.text
response = client.get(f"/heroes/{hero_id}")
assert response.status_code == 200, response.text
response = client.get("/heroes/9000")
assert response.status_code == 404, response.text
response = client.get("/openapi.json")
data = response.json()
assert response.status_code == 200, response.text
assert data == openapi_schema
response = client.get("/heroes/")
assert response.status_code == 200, response.text
data = response.json()
assert len(data) == 3
response = client.get("/heroes/", params={"limit": 2})
assert response.status_code == 200, response.text
data = response.json()
assert len(data) == 2
assert data[0]["name"] == hero1_data["name"]
assert data[1]["name"] == hero2_data["name"]
response = client.get("/heroes/", params={"offset": 1})
assert response.status_code == 200, response.text
data = response.json()
assert len(data) == 2
assert data[0]["name"] == hero2_data["name"]
assert data[1]["name"] == hero3_data["name"]
response = client.get("/heroes/", params={"offset": 1, "limit": 1})
assert response.status_code == 200, response.text
data = response.json()
assert len(data) == 1
assert data[0]["name"] == hero2_data["name"]

View File

@ -123,7 +123,6 @@ def test_tutorial(clear_sqlmodel):
) )
with TestClient(mod.app) as client: with TestClient(mod.app) as client:
hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"}
hero2_data = { hero2_data = {
"name": "Spider-Boy", "name": "Spider-Boy",
@ -173,8 +172,18 @@ def test_tutorial(clear_sqlmodel):
insp: Inspector = inspect(mod.engine) insp: Inspector = inspect(mod.engine)
indexes = insp.get_indexes(str(mod.Hero.__tablename__)) indexes = insp.get_indexes(str(mod.Hero.__tablename__))
expected_indexes = [ expected_indexes = [
{"name": "ix_hero_name", "column_names": ["name"], "unique": 0}, {
{"name": "ix_hero_age", "column_names": ["age"], "unique": 0}, "name": "ix_hero_name",
"dialect_options": {},
"column_names": ["name"],
"unique": 0,
},
{
"name": "ix_hero_age",
"dialect_options": {},
"column_names": ["age"],
"unique": 0,
},
] ]
for index in expected_indexes: for index in expected_indexes:
assert index in indexes, "This expected index should be in the indexes in DB" assert index in indexes, "This expected index should be in the indexes in DB"

View File

@ -123,7 +123,6 @@ def test_tutorial(clear_sqlmodel):
) )
with TestClient(mod.app) as client: with TestClient(mod.app) as client:
hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"}
hero2_data = { hero2_data = {
"name": "Spider-Boy", "name": "Spider-Boy",
@ -173,8 +172,18 @@ def test_tutorial(clear_sqlmodel):
insp: Inspector = inspect(mod.engine) insp: Inspector = inspect(mod.engine)
indexes = insp.get_indexes(str(mod.Hero.__tablename__)) indexes = insp.get_indexes(str(mod.Hero.__tablename__))
expected_indexes = [ expected_indexes = [
{"name": "ix_hero_age", "column_names": ["age"], "unique": 0}, {
{"name": "ix_hero_name", "column_names": ["name"], "unique": 0}, "name": "ix_hero_age",
"dialect_options": {},
"column_names": ["age"],
"unique": 0,
},
{
"name": "ix_hero_name",
"dialect_options": {},
"column_names": ["name"],
"unique": 0,
},
] ]
for index in expected_indexes: for index in expected_indexes:
assert index in indexes, "This expected index should be in the indexes in DB" assert index in indexes, "This expected index should be in the indexes in DB"

View File

@ -155,7 +155,6 @@ def test_tutorial(clear_sqlmodel):
) )
with TestClient(mod.app) as client: with TestClient(mod.app) as client:
hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"}
hero2_data = { hero2_data = {
"name": "Spider-Boy", "name": "Spider-Boy",

View File

@ -2,7 +2,111 @@ from fastapi.testclient import TestClient
from sqlmodel import create_engine from sqlmodel import create_engine
from sqlmodel.pool import StaticPool from sqlmodel.pool import StaticPool
openapi_schema = {
def test_tutorial(clear_sqlmodel):
from docs_src.tutorial.fastapi.relationships import tutorial001 as mod
mod.sqlite_url = "sqlite://"
mod.engine = create_engine(
mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool
)
with TestClient(mod.app) as client:
team_preventers = {"name": "Preventers", "headquarters": "Sharp Tower"}
team_z_force = {"name": "Z-Force", "headquarters": "Sister Margarets Bar"}
response = client.post("/teams/", json=team_preventers)
assert response.status_code == 200, response.text
team_preventers_data = response.json()
team_preventers_id = team_preventers_data["id"]
response = client.post("/teams/", json=team_z_force)
assert response.status_code == 200, response.text
team_z_force_data = response.json()
team_z_force_id = team_z_force_data["id"]
response = client.get("/teams/")
data = response.json()
assert len(data) == 2
response = client.get("/teams/9000")
assert response.status_code == 404, response.text
response = client.patch(
f"/teams/{team_preventers_id}", json={"headquarters": "Preventers Tower"}
)
data = response.json()
assert response.status_code == 200, response.text
assert data["name"] == team_preventers["name"]
assert data["headquarters"] == "Preventers Tower"
response = client.patch("/teams/9000", json={"name": "Freedom League"})
assert response.status_code == 404, response.text
hero1_data = {
"name": "Deadpond",
"secret_name": "Dive Wilson",
"team_id": team_z_force_id,
}
hero2_data = {
"name": "Spider-Boy",
"secret_name": "Pedro Parqueador",
"id": 9000,
}
hero3_data = {
"name": "Rusty-Man",
"secret_name": "Tommy Sharp",
"age": 48,
"team_id": team_preventers_id,
}
response = client.post("/heroes/", json=hero1_data)
assert response.status_code == 200, response.text
hero1 = response.json()
hero1_id = hero1["id"]
response = client.post("/heroes/", json=hero2_data)
assert response.status_code == 200, response.text
hero2 = response.json()
hero2_id = hero2["id"]
response = client.post("/heroes/", json=hero3_data)
assert response.status_code == 200, response.text
response = client.get("/heroes/9000")
assert response.status_code == 404, response.text
response = client.get("/heroes/")
assert response.status_code == 200, response.text
data = response.json()
assert len(data) == 3
response = client.get(f"/heroes/{hero1_id}")
assert response.status_code == 200, response.text
data = response.json()
assert data["name"] == hero1_data["name"]
assert data["team"]["name"] == team_z_force["name"]
response = client.patch(
f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"}
)
assert response.status_code == 200, response.text
response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"})
assert response.status_code == 404, response.text
response = client.delete(f"/heroes/{hero2_id}")
assert response.status_code == 200, response.text
response = client.get("/heroes/")
assert response.status_code == 200, response.text
data = response.json()
assert len(data) == 2
response = client.delete("/heroes/9000")
assert response.status_code == 404, response.text
response = client.get(f"/teams/{team_preventers_id}")
data = response.json()
assert response.status_code == 200, response.text
assert data["name"] == team_preventers_data["name"]
assert data["heroes"][0]["name"] == hero3_data["name"]
response = client.delete(f"/teams/{team_preventers_id}")
assert response.status_code == 200, response.text
response = client.delete("/teams/9000")
assert response.status_code == 404, response.text
response = client.get("/teams/")
assert response.status_code == 200, response.text
data = response.json()
assert len(data) == 1
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
"openapi": "3.0.2", "openapi": "3.0.2",
"info": {"title": "FastAPI", "version": "0.1.0"}, "info": {"title": "FastAPI", "version": "0.1.0"},
"paths": { "paths": {
@ -13,7 +117,11 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": False, "required": False,
"schema": {"title": "Offset", "type": "integer", "default": 0}, "schema": {
"title": "Offset",
"type": "integer",
"default": 0,
},
"name": "offset", "name": "offset",
"in": "query", "in": "query",
}, },
@ -21,9 +129,9 @@ openapi_schema = {
"required": False, "required": False,
"schema": { "schema": {
"title": "Limit", "title": "Limit",
"maximum": 100.0,
"type": "integer", "type": "integer",
"default": 100, "default": 100,
"lte": 100,
}, },
"name": "limit", "name": "limit",
"in": "query", "in": "query",
@ -37,7 +145,9 @@ openapi_schema = {
"schema": { "schema": {
"title": "Response Read Heroes Heroes Get", "title": "Response Read Heroes Heroes Get",
"type": "array", "type": "array",
"items": {"$ref": "#/components/schemas/HeroRead"}, "items": {
"$ref": "#/components/schemas/HeroRead"
},
} }
} }
}, },
@ -60,7 +170,9 @@ openapi_schema = {
"requestBody": { "requestBody": {
"content": { "content": {
"application/json": { "application/json": {
"schema": {"$ref": "#/components/schemas/HeroCreate"} "schema": {
"$ref": "#/components/schemas/HeroCreate"
}
} }
}, },
"required": True, "required": True,
@ -70,7 +182,9 @@ openapi_schema = {
"description": "Successful Response", "description": "Successful Response",
"content": { "content": {
"application/json": { "application/json": {
"schema": {"$ref": "#/components/schemas/HeroRead"} "schema": {
"$ref": "#/components/schemas/HeroRead"
}
} }
}, },
}, },
@ -164,7 +278,9 @@ openapi_schema = {
"requestBody": { "requestBody": {
"content": { "content": {
"application/json": { "application/json": {
"schema": {"$ref": "#/components/schemas/HeroUpdate"} "schema": {
"$ref": "#/components/schemas/HeroUpdate"
}
} }
}, },
"required": True, "required": True,
@ -174,7 +290,9 @@ openapi_schema = {
"description": "Successful Response", "description": "Successful Response",
"content": { "content": {
"application/json": { "application/json": {
"schema": {"$ref": "#/components/schemas/HeroRead"} "schema": {
"$ref": "#/components/schemas/HeroRead"
}
} }
}, },
}, },
@ -198,7 +316,11 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": False, "required": False,
"schema": {"title": "Offset", "type": "integer", "default": 0}, "schema": {
"title": "Offset",
"type": "integer",
"default": 0,
},
"name": "offset", "name": "offset",
"in": "query", "in": "query",
}, },
@ -206,9 +328,9 @@ openapi_schema = {
"required": False, "required": False,
"schema": { "schema": {
"title": "Limit", "title": "Limit",
"maximum": 100.0,
"type": "integer", "type": "integer",
"default": 100, "default": 100,
"lte": 100,
}, },
"name": "limit", "name": "limit",
"in": "query", "in": "query",
@ -222,7 +344,9 @@ openapi_schema = {
"schema": { "schema": {
"title": "Response Read Teams Teams Get", "title": "Response Read Teams Teams Get",
"type": "array", "type": "array",
"items": {"$ref": "#/components/schemas/TeamRead"}, "items": {
"$ref": "#/components/schemas/TeamRead"
},
} }
} }
}, },
@ -245,7 +369,9 @@ openapi_schema = {
"requestBody": { "requestBody": {
"content": { "content": {
"application/json": { "application/json": {
"schema": {"$ref": "#/components/schemas/TeamCreate"} "schema": {
"$ref": "#/components/schemas/TeamCreate"
}
} }
}, },
"required": True, "required": True,
@ -255,7 +381,9 @@ openapi_schema = {
"description": "Successful Response", "description": "Successful Response",
"content": { "content": {
"application/json": { "application/json": {
"schema": {"$ref": "#/components/schemas/TeamRead"} "schema": {
"$ref": "#/components/schemas/TeamRead"
}
} }
}, },
}, },
@ -349,7 +477,9 @@ openapi_schema = {
"requestBody": { "requestBody": {
"content": { "content": {
"application/json": { "application/json": {
"schema": {"$ref": "#/components/schemas/TeamUpdate"} "schema": {
"$ref": "#/components/schemas/TeamUpdate"
}
} }
}, },
"required": True, "required": True,
@ -359,7 +489,9 @@ openapi_schema = {
"description": "Successful Response", "description": "Successful Response",
"content": { "content": {
"application/json": { "application/json": {
"schema": {"$ref": "#/components/schemas/TeamRead"} "schema": {
"$ref": "#/components/schemas/TeamRead"
}
} }
}, },
}, },
@ -386,7 +518,9 @@ openapi_schema = {
"detail": { "detail": {
"title": "Detail", "title": "Detail",
"type": "array", "type": "array",
"items": {"$ref": "#/components/schemas/ValidationError"}, "items": {
"$ref": "#/components/schemas/ValidationError"
},
} }
}, },
}, },
@ -497,110 +631,3 @@ openapi_schema = {
} }
}, },
} }
def test_tutorial(clear_sqlmodel):
from docs_src.tutorial.fastapi.relationships import tutorial001 as mod
mod.sqlite_url = "sqlite://"
mod.engine = create_engine(
mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool
)
with TestClient(mod.app) as client:
response = client.get("/openapi.json")
data = response.json()
assert response.status_code == 200, response.text
assert data == openapi_schema
team_preventers = {"name": "Preventers", "headquarters": "Sharp Tower"}
team_z_force = {"name": "Z-Force", "headquarters": "Sister Margarets Bar"}
response = client.post("/teams/", json=team_preventers)
assert response.status_code == 200, response.text
team_preventers_data = response.json()
team_preventers_id = team_preventers_data["id"]
response = client.post("/teams/", json=team_z_force)
assert response.status_code == 200, response.text
team_z_force_data = response.json()
team_z_force_id = team_z_force_data["id"]
response = client.get("/teams/")
data = response.json()
assert len(data) == 2
response = client.get("/teams/9000")
assert response.status_code == 404, response.text
response = client.patch(
f"/teams/{team_preventers_id}", json={"headquarters": "Preventers Tower"}
)
data = response.json()
assert response.status_code == 200, response.text
assert data["name"] == team_preventers["name"]
assert data["headquarters"] == "Preventers Tower"
response = client.patch("/teams/9000", json={"name": "Freedom League"})
assert response.status_code == 404, response.text
hero1_data = {
"name": "Deadpond",
"secret_name": "Dive Wilson",
"team_id": team_z_force_id,
}
hero2_data = {
"name": "Spider-Boy",
"secret_name": "Pedro Parqueador",
"id": 9000,
}
hero3_data = {
"name": "Rusty-Man",
"secret_name": "Tommy Sharp",
"age": 48,
"team_id": team_preventers_id,
}
response = client.post("/heroes/", json=hero1_data)
assert response.status_code == 200, response.text
hero1 = response.json()
hero1_id = hero1["id"]
response = client.post("/heroes/", json=hero2_data)
assert response.status_code == 200, response.text
hero2 = response.json()
hero2_id = hero2["id"]
response = client.post("/heroes/", json=hero3_data)
assert response.status_code == 200, response.text
response = client.get("/heroes/9000")
assert response.status_code == 404, response.text
response = client.get("/heroes/")
assert response.status_code == 200, response.text
data = response.json()
assert len(data) == 3
response = client.get(f"/heroes/{hero1_id}")
assert response.status_code == 200, response.text
data = response.json()
assert data["name"] == hero1_data["name"]
assert data["team"]["name"] == team_z_force["name"]
response = client.patch(
f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"}
)
assert response.status_code == 200, response.text
response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"})
assert response.status_code == 404, response.text
response = client.delete(f"/heroes/{hero2_id}")
assert response.status_code == 200, response.text
response = client.get("/heroes/")
assert response.status_code == 200, response.text
data = response.json()
assert len(data) == 2
response = client.delete("/heroes/9000")
assert response.status_code == 404, response.text
response = client.get(f"/teams/{team_preventers_id}")
data = response.json()
assert response.status_code == 200, response.text
assert data["name"] == team_preventers_data["name"]
assert data["heroes"][0]["name"] == hero3_data["name"]
response = client.delete(f"/teams/{team_preventers_id}")
assert response.status_code == 200, response.text
response = client.delete("/teams/9000")
assert response.status_code == 404, response.text
response = client.get("/teams/")
assert response.status_code == 200, response.text
data = response.json()
assert len(data) == 1

View File

@ -111,7 +111,6 @@ def test_tutorial(clear_sqlmodel):
) )
with TestClient(mod.app) as client: with TestClient(mod.app) as client:
hero_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} hero_data = {"name": "Deadpond", "secret_name": "Dive Wilson"}
response = client.post("/heroes/", json=hero_data) response = client.post("/heroes/", json=hero_data)
data = response.json() data = response.json()

View File

@ -2,7 +2,63 @@ from fastapi.testclient import TestClient
from sqlmodel import create_engine from sqlmodel import create_engine
from sqlmodel.pool import StaticPool from sqlmodel.pool import StaticPool
openapi_schema = {
def test_tutorial(clear_sqlmodel):
from docs_src.tutorial.fastapi.session_with_dependency import tutorial001 as mod
mod.sqlite_url = "sqlite://"
mod.engine = create_engine(
mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool
)
with TestClient(mod.app) as client:
hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"}
hero2_data = {
"name": "Spider-Boy",
"secret_name": "Pedro Parqueador",
"id": 9000,
}
hero3_data = {
"name": "Rusty-Man",
"secret_name": "Tommy Sharp",
"age": 48,
}
response = client.post("/heroes/", json=hero1_data)
assert response.status_code == 200, response.text
response = client.post("/heroes/", json=hero2_data)
assert response.status_code == 200, response.text
hero2 = response.json()
hero2_id = hero2["id"]
response = client.post("/heroes/", json=hero3_data)
assert response.status_code == 200, response.text
response = client.get(f"/heroes/{hero2_id}")
assert response.status_code == 200, response.text
response = client.get("/heroes/9000")
assert response.status_code == 404, response.text
response = client.get("/heroes/")
assert response.status_code == 200, response.text
data = response.json()
assert len(data) == 3
response = client.patch(
f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"}
)
assert response.status_code == 200, response.text
response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"})
assert response.status_code == 404, response.text
response = client.delete(f"/heroes/{hero2_id}")
assert response.status_code == 200, response.text
response = client.get("/heroes/")
assert response.status_code == 200, response.text
data = response.json()
assert len(data) == 2
response = client.delete("/heroes/9000")
assert response.status_code == 404, response.text
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
"openapi": "3.0.2", "openapi": "3.0.2",
"info": {"title": "FastAPI", "version": "0.1.0"}, "info": {"title": "FastAPI", "version": "0.1.0"},
"paths": { "paths": {
@ -13,7 +69,11 @@ openapi_schema = {
"parameters": [ "parameters": [
{ {
"required": False, "required": False,
"schema": {"title": "Offset", "type": "integer", "default": 0}, "schema": {
"title": "Offset",
"type": "integer",
"default": 0,
},
"name": "offset", "name": "offset",
"in": "query", "in": "query",
}, },
@ -21,9 +81,9 @@ openapi_schema = {
"required": False, "required": False,
"schema": { "schema": {
"title": "Limit", "title": "Limit",
"maximum": 100.0,
"type": "integer", "type": "integer",
"default": 100, "default": 100,
"lte": 100,
}, },
"name": "limit", "name": "limit",
"in": "query", "in": "query",
@ -37,7 +97,9 @@ openapi_schema = {
"schema": { "schema": {
"title": "Response Read Heroes Heroes Get", "title": "Response Read Heroes Heroes Get",
"type": "array", "type": "array",
"items": {"$ref": "#/components/schemas/HeroRead"}, "items": {
"$ref": "#/components/schemas/HeroRead"
},
} }
} }
}, },
@ -60,7 +122,9 @@ openapi_schema = {
"requestBody": { "requestBody": {
"content": { "content": {
"application/json": { "application/json": {
"schema": {"$ref": "#/components/schemas/HeroCreate"} "schema": {
"$ref": "#/components/schemas/HeroCreate"
}
} }
}, },
"required": True, "required": True,
@ -70,7 +134,9 @@ openapi_schema = {
"description": "Successful Response", "description": "Successful Response",
"content": { "content": {
"application/json": { "application/json": {
"schema": {"$ref": "#/components/schemas/HeroRead"} "schema": {
"$ref": "#/components/schemas/HeroRead"
}
} }
}, },
}, },
@ -104,7 +170,9 @@ openapi_schema = {
"description": "Successful Response", "description": "Successful Response",
"content": { "content": {
"application/json": { "application/json": {
"schema": {"$ref": "#/components/schemas/HeroRead"} "schema": {
"$ref": "#/components/schemas/HeroRead"
}
} }
}, },
}, },
@ -162,7 +230,9 @@ openapi_schema = {
"requestBody": { "requestBody": {
"content": { "content": {
"application/json": { "application/json": {
"schema": {"$ref": "#/components/schemas/HeroUpdate"} "schema": {
"$ref": "#/components/schemas/HeroUpdate"
}
} }
}, },
"required": True, "required": True,
@ -172,7 +242,9 @@ openapi_schema = {
"description": "Successful Response", "description": "Successful Response",
"content": { "content": {
"application/json": { "application/json": {
"schema": {"$ref": "#/components/schemas/HeroRead"} "schema": {
"$ref": "#/components/schemas/HeroRead"
}
} }
}, },
}, },
@ -199,7 +271,9 @@ openapi_schema = {
"detail": { "detail": {
"title": "Detail", "title": "Detail",
"type": "array", "type": "array",
"items": {"$ref": "#/components/schemas/ValidationError"}, "items": {
"$ref": "#/components/schemas/ValidationError"
},
} }
}, },
}, },
@ -250,62 +324,3 @@ openapi_schema = {
} }
}, },
} }
def test_tutorial(clear_sqlmodel):
from docs_src.tutorial.fastapi.session_with_dependency import tutorial001 as mod
mod.sqlite_url = "sqlite://"
mod.engine = create_engine(
mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool
)
with TestClient(mod.app) as client:
hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"}
hero2_data = {
"name": "Spider-Boy",
"secret_name": "Pedro Parqueador",
"id": 9000,
}
hero3_data = {
"name": "Rusty-Man",
"secret_name": "Tommy Sharp",
"age": 48,
}
response = client.post("/heroes/", json=hero1_data)
assert response.status_code == 200, response.text
response = client.post("/heroes/", json=hero2_data)
assert response.status_code == 200, response.text
hero2 = response.json()
hero2_id = hero2["id"]
response = client.post("/heroes/", json=hero3_data)
assert response.status_code == 200, response.text
response = client.get(f"/heroes/{hero2_id}")
assert response.status_code == 200, response.text
response = client.get("/heroes/9000")
assert response.status_code == 404, response.text
response = client.get("/openapi.json")
data = response.json()
assert response.status_code == 200, response.text
assert data == openapi_schema
response = client.get("/heroes/")
assert response.status_code == 200, response.text
data = response.json()
assert len(data) == 3
response = client.patch(
f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"}
)
assert response.status_code == 200, response.text
response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"})
assert response.status_code == 404, response.text
response = client.delete(f"/heroes/{hero2_id}")
assert response.status_code == 200, response.text
response = client.get("/heroes/")
assert response.status_code == 200, response.text
data = response.json()
assert len(data) == 2
response = client.delete("/heroes/9000")
assert response.status_code == 404, response.text

View File

@ -99,7 +99,6 @@ def test_tutorial(clear_sqlmodel):
) )
with TestClient(mod.app) as client: with TestClient(mod.app) as client:
hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"}
hero2_data = { hero2_data = {
"name": "Spider-Boy", "name": "Spider-Boy",

Some files were not shown because too many files have changed in this diff Show More