Merge branch 'main' into main
This commit is contained in:
commit
a1833901f5
@ -1,5 +1,3 @@
|
||||
name: Question or Problem
|
||||
description: Ask a question or ask about a problem
|
||||
labels: [question]
|
||||
body:
|
||||
- type: markdown
|
||||
@ -9,9 +7,9 @@ body:
|
||||
|
||||
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.
|
||||
|
||||
@ -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. 😎
|
||||
|
||||
As there are too many issues with questions, I'll have to close the incomplete ones. That will allow me (and others) to focus on helping people like you that follow the whole process and help us help you. 🤓
|
||||
As there are too many questions, I'll have to discard and close the incomplete ones. That will allow me (and others) to focus on helping people like you that follow the whole process and help us help you. 🤓
|
||||
- type: checkboxes
|
||||
id: checks
|
||||
attributes:
|
||||
label: First Check
|
||||
description: Please confirm and check all the following options.
|
||||
options:
|
||||
- label: I added a very descriptive title to this issue.
|
||||
- label: I added a very descriptive title here.
|
||||
required: true
|
||||
- label: I used the GitHub search to find a similar issue and didn't find it.
|
||||
- label: I used the GitHub search to find a similar question and didn't find it.
|
||||
required: true
|
||||
- label: I searched the SQLModel documentation, with the integrated search.
|
||||
required: true
|
||||
@ -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.
|
||||
* I already hit the "watch" button in this repository to receive notifications and I commit to help at least 2 people that ask questions in the future.
|
||||
* Implement a Pull Request for a confirmed bug.
|
||||
* Review one Pull Request by downloading the code and following all the review process](https://sqlmodel.tiangolo.com/help/#review-pull-requests).
|
||||
|
||||
options:
|
||||
- label: I commit to help with one of those options 👆
|
9
.github/ISSUE_TEMPLATE/config.yml
vendored
9
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -2,3 +2,12 @@ blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Security Contact
|
||||
about: Please report security vulnerabilities to security@tiangolo.com
|
||||
- name: Question or Problem
|
||||
about: Ask a question or ask about a problem in GitHub Discussions.
|
||||
url: https://github.com/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
|
||||
|
214
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
214
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
@ -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
22
.github/ISSUE_TEMPLATE/privileged.yml
vendored
Normal 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.
|
@ -48,9 +48,7 @@ if __name__ == "__main__":
|
||||
use_pr = pr
|
||||
break
|
||||
if not use_pr:
|
||||
logging.error(
|
||||
f"No PR found for hash: {event.workflow_run.head_commit.id}"
|
||||
)
|
||||
logging.error(f"No PR found for hash: {event.workflow_run.head_commit.id}")
|
||||
sys.exit(0)
|
||||
github_headers = {
|
||||
"Authorization": f"token {settings.input_token.get_secret_value()}"
|
||||
|
7
.github/actions/watch-previews/Dockerfile
vendored
7
.github/actions/watch-previews/Dockerfile
vendored
@ -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"]
|
10
.github/actions/watch-previews/action.yml
vendored
10
.github/actions/watch-previews/action.yml
vendored
@ -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
|
102
.github/actions/watch-previews/app/main.py
vendored
102
.github/actions/watch-previews/app/main.py
vendored
@ -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")
|
95
.github/workflows/build-docs.yml
vendored
95
.github/workflows/build-docs.yml
vendored
@ -4,78 +4,91 @@ on:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
debug_enabled:
|
||||
description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
|
||||
required: false
|
||||
default: false
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
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:
|
||||
runs-on: ubuntu-20.04
|
||||
needs:
|
||||
- changes
|
||||
if: ${{ needs.changes.outputs.docs == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Dump GitHub context
|
||||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@v3.1.0
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.7"
|
||||
# Allow debugging with tmate
|
||||
- name: Setup tmate session
|
||||
uses: mxschmitt/action-tmate@v3
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }}
|
||||
with:
|
||||
limit-access-to-actor: true
|
||||
python-version: "3.11"
|
||||
- uses: actions/cache@v3
|
||||
id: cache
|
||||
with:
|
||||
path: ${{ env.pythonLocation }}
|
||||
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-root-docs
|
||||
- name: Install poetry
|
||||
key: ${{ runner.os }}-python-docs-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-v01
|
||||
- name: Install Poetry
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
# TODO: remove python -m pip install --force git+https://github.com/python-poetry/poetry-core.git@ad33bc2
|
||||
# once there's a release of Poetry 1.2.x including poetry-core > 1.1.0a6
|
||||
# Ref: https://github.com/python-poetry/poetry-core/pull/188
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install --force git+https://github.com/python-poetry/poetry-core.git@ad33bc2
|
||||
python -m pip install "poetry==1.2.0a2"
|
||||
python -m poetry plugin add poetry-version-plugin
|
||||
python -m pip install "poetry"
|
||||
python -m poetry self add poetry-version-plugin
|
||||
- name: Configure poetry
|
||||
run: python -m poetry config virtualenvs.create false
|
||||
- name: Install Dependencies
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
run: python -m poetry install
|
||||
- name: Install Material for MkDocs Insiders
|
||||
if: github.event.pull_request.head.repo.fork == false && steps.cache.outputs.cache-hit != 'true'
|
||||
run: python -m poetry run pip install git+https://${{ secrets.ACTIONS_TOKEN }}@github.com/squidfunk/mkdocs-material-insiders.git
|
||||
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.SQLMODEL_MKDOCS_MATERIAL_INSIDERS }}@github.com/squidfunk/mkdocs-material-insiders.git
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
key: mkdocs-cards-${{ github.ref }}
|
||||
path: .cache
|
||||
- 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
|
||||
- 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
|
||||
- name: Zip docs
|
||||
run: python -m poetry run bash ./scripts/zip-docs.sh
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: docs-zip
|
||||
path: ./site/docs.zip
|
||||
- name: Deploy to Netlify
|
||||
uses: nwtgck/actions-netlify@v1.1.5
|
||||
name: docs-site
|
||||
path: ./site/**
|
||||
|
||||
# https://github.com/marketplace/actions/alls-green#why
|
||||
docs-all-green: # This job does nothing and is only used for the branch protection
|
||||
if: always()
|
||||
needs:
|
||||
- build-docs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Decide whether the needed jobs succeeded or failed
|
||||
uses: re-actors/alls-green@release/v1
|
||||
with:
|
||||
publish-dir: './site'
|
||||
production-branch: main
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
enable-commit-comment: false
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
|
||||
jobs: ${{ toJSON(needs) }}
|
||||
allowed-skips: build-docs
|
||||
|
48
.github/workflows/deploy-docs.yml
vendored
Normal file
48
.github/workflows/deploy-docs.yml
vendored
Normal 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 }}"
|
8
.github/workflows/latest-changes.yml
vendored
8
.github/workflows/latest-changes.yml
vendored
@ -14,20 +14,20 @@ on:
|
||||
debug_enabled:
|
||||
description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
|
||||
required: false
|
||||
default: false
|
||||
default: 'false'
|
||||
|
||||
jobs:
|
||||
latest-changes:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3.1.0
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# To allow latest-changes to commit to the main branch
|
||||
token: ${{ secrets.ACTIONS_TOKEN }}
|
||||
token: ${{ secrets.SQLMODEL_LATEST_CHANGES }}
|
||||
# Allow debugging with tmate
|
||||
- name: Setup tmate session
|
||||
uses: mxschmitt/action-tmate@v3
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }}
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }}
|
||||
with:
|
||||
limit-access-to-actor: true
|
||||
- uses: docker://tiangolo/latest-changes:0.0.3
|
||||
|
46
.github/workflows/preview-docs.yml
vendored
46
.github/workflows/preview-docs.yml
vendored
@ -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 }}"
|
18
.github/workflows/publish.yml
vendored
18
.github/workflows/publish.yml
vendored
@ -9,13 +9,13 @@ on:
|
||||
debug_enabled:
|
||||
description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
|
||||
required: false
|
||||
default: false
|
||||
default: 'false'
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3.1.0
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
@ -23,24 +23,20 @@ jobs:
|
||||
# Allow debugging with tmate
|
||||
- name: Setup tmate session
|
||||
uses: mxschmitt/action-tmate@v3
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }}
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }}
|
||||
with:
|
||||
limit-access-to-actor: true
|
||||
- uses: actions/cache@v3
|
||||
id: cache
|
||||
with:
|
||||
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
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
# TODO: remove python -m pip install --force git+https://github.com/python-poetry/poetry-core.git@ad33bc2
|
||||
# once there's a release of Poetry 1.2.x including poetry-core > 1.1.0a6
|
||||
# Ref: https://github.com/python-poetry/poetry-core/pull/188
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install --force git+https://github.com/python-poetry/poetry-core.git@ad33bc2
|
||||
python -m pip install "poetry==1.2.0a2"
|
||||
python -m poetry plugin add poetry-version-plugin
|
||||
python -m pip install "poetry"
|
||||
python -m poetry self add poetry-version-plugin
|
||||
- name: Configure poetry
|
||||
run: python -m poetry config virtualenvs.create false
|
||||
- name: Install Dependencies
|
||||
|
2
.github/workflows/smokeshow.yml
vendored
2
.github/workflows/smokeshow.yml
vendored
@ -20,7 +20,7 @@ jobs:
|
||||
|
||||
- run: pip install smokeshow
|
||||
|
||||
- uses: dawidd6/action-download-artifact@v2.24.2
|
||||
- uses: dawidd6/action-download-artifact@v2.28.0
|
||||
with:
|
||||
workflow: test.yml
|
||||
commit: ${{ github.event.workflow_run.head_sha }}
|
||||
|
46
.github/workflows/test.yml
vendored
46
.github/workflows/test.yml
vendored
@ -5,24 +5,30 @@ on:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
debug_enabled:
|
||||
description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
|
||||
required: false
|
||||
default: false
|
||||
default: 'false'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
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
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3.1.0
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
@ -30,31 +36,26 @@ jobs:
|
||||
# Allow debugging with tmate
|
||||
- name: Setup tmate session
|
||||
uses: mxschmitt/action-tmate@v3
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }}
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }}
|
||||
with:
|
||||
limit-access-to-actor: true
|
||||
- uses: actions/cache@v3
|
||||
id: cache
|
||||
with:
|
||||
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
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
# TODO: remove python -m pip install --force git+https://github.com/python-poetry/poetry-core.git@ad33bc2
|
||||
# once there's a release of Poetry 1.2.x including poetry-core > 1.1.0a6
|
||||
# Ref: https://github.com/python-poetry/poetry-core/pull/188
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install --force git+https://github.com/python-poetry/poetry-core.git@ad33bc2
|
||||
python -m pip install "poetry==1.2.0a2"
|
||||
python -m poetry plugin add poetry-version-plugin
|
||||
python -m pip install "poetry"
|
||||
python -m poetry self add poetry-version-plugin
|
||||
- name: Configure poetry
|
||||
run: python -m poetry config virtualenvs.create false
|
||||
- name: Install Dependencies
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
run: python -m poetry install
|
||||
- name: Lint
|
||||
if: ${{ matrix.python-version != '3.6.15' }}
|
||||
run: python -m poetry run bash scripts/lint.sh
|
||||
- run: mkdir coverage
|
||||
- name: Test
|
||||
@ -68,11 +69,12 @@ jobs:
|
||||
name: coverage
|
||||
path: coverage
|
||||
coverage-combine:
|
||||
needs: [test]
|
||||
needs:
|
||||
- test
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
@ -96,3 +98,15 @@ jobs:
|
||||
with:
|
||||
name: coverage-html
|
||||
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
25
.pre-commit-config.yaml
Normal 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
24
CITATION.cff
Normal 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
|
@ -50,7 +50,7 @@ It combines SQLAlchemy and Pydantic and tries to simplify the code you write as
|
||||
|
||||
## Requirements
|
||||
|
||||
A recent and currently supported version of Python (right now, <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.
|
||||
|
||||
|
@ -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.
|
||||
|
||||
### Python
|
||||
|
||||
SQLModel supports Python 3.6 and above, but for development you should have at least **Python 3.7**.
|
||||
|
||||
### Poetry
|
||||
|
||||
**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">
|
||||
|
||||
```console
|
||||
$ bash scripts/test-cov-html.sh
|
||||
$ bash scripts/test.sh
|
||||
```
|
||||
|
||||
</div>
|
||||
|
@ -111,7 +111,7 @@ DROP 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
|
||||
|
||||
|
@ -12,7 +12,7 @@ Nevertheless, SQLModel is completely **independent** of FastAPI and can be used
|
||||
|
||||
## 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>.
|
||||
|
||||
|
150
docs/help.md
150
docs/help.md
@ -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.
|
||||
|
||||
## 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
|
||||
|
||||
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**.
|
||||
* Suggest a new **feature**.
|
||||
|
||||
**Note**: if you create an issue, then I'm going to ask you to also help others. 😉
|
||||
**Note**: if you do it, then I'm going to ask you to also help others. 😉
|
||||
|
||||
## Review Pull Requests
|
||||
|
||||
You can help me review pull requests from others.
|
||||
|
||||
Again, please try your best to be kind. 🤗
|
||||
|
||||
---
|
||||
|
||||
Here's what to have in mind and how to review a pull request:
|
||||
|
||||
### Understand the problem
|
||||
|
||||
* First, make sure you **understand the problem** that the pull request is trying to solve. It might have a longer discussion in a GitHub Discussion or issue.
|
||||
|
||||
* There's also a good chance that the pull request is not actually needed because the problem can be solved in a **different way**. Then you can suggest or ask about that.
|
||||
|
||||
### Don't worry about style
|
||||
|
||||
* Don't worry too much about things like commit message styles, I will squash and merge customizing the commit manually.
|
||||
|
||||
* Also don't worry about style rules, there are already automatized tools checking that.
|
||||
|
||||
And if there's any other style or consistency need, I'll ask directly for that, or I'll add commits on top with the needed changes.
|
||||
|
||||
### Check the code
|
||||
|
||||
* Check and read the code, see if it makes sense, **run it locally** and see if it actually solves the problem.
|
||||
|
||||
* Then **comment** saying that you did that, that's how I will know you really checked it.
|
||||
|
||||
!!! info
|
||||
Unfortunately, I can't simply trust PRs that just have several approvals.
|
||||
|
||||
Several times it has happened that there are PRs with 3, 5 or more approvals, probably because the description is appealing, but when I check the PRs, they are actually broken, have a bug, or don't solve the problem they claim to solve. 😅
|
||||
|
||||
So, it's really important that you actually read and run the code, and let me know in the comments that you did. 🤓
|
||||
|
||||
* If the PR can be simplified in a way, you can ask for that, but there's no need to be too picky, there might be a lot of subjective points of view (and I will have my own as well 🙈), so it's better if you can focus on the fundamental things.
|
||||
|
||||
### Tests
|
||||
|
||||
* Help me check that the PR has **tests**.
|
||||
|
||||
* Check that the tests **fail** before the PR. 🚨
|
||||
|
||||
* Then check that the tests **pass** after the PR. ✅
|
||||
|
||||
* Many PRs don't have tests, you can **remind** them to add tests, or you can even **suggest** some tests yourself. That's one of the things that consume most time and you can help a lot with that.
|
||||
|
||||
* Then also comment what you tried, that way I'll know that you checked it. 🤓
|
||||
|
||||
## Create a Pull Request
|
||||
|
||||
@ -86,7 +185,44 @@ You can [contribute](contributing.md){.internal-link target=_blank} to the sourc
|
||||
* To fix a typo you found on the documentation.
|
||||
* To propose new documentation sections.
|
||||
* To fix an existing issue/bug.
|
||||
* Make sure to add tests.
|
||||
* To add a new feature.
|
||||
* Make sure to add tests.
|
||||
* Make sure to add documentation if it's relevant.
|
||||
|
||||
## Help Maintain SQLModel
|
||||
|
||||
Help me maintain **SQLModel**! 🤓
|
||||
|
||||
There's a lot of work to do, and for most of it, **YOU** can do it.
|
||||
|
||||
The main tasks that you can do right now are:
|
||||
|
||||
* [Help others with questions in GitHub](#help-others-with-questions-in-github){.internal-link target=_blank} (see the section above).
|
||||
* [Review Pull Requests](#review-pull-requests){.internal-link target=_blank} (see the section above).
|
||||
|
||||
Those two tasks are what **consume time the most**. That's the main work of maintaining SQLModel.
|
||||
|
||||
If you can help me with that, **you are helping me maintain SQLModel** and making sure it keeps **advancing faster and better**. 🚀
|
||||
|
||||
## Join the chat
|
||||
|
||||
Join the 👥 <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
|
||||
|
||||
|
@ -50,7 +50,7 @@ It combines SQLAlchemy and Pydantic and tries to simplify the code you write as
|
||||
|
||||
## Requirements
|
||||
|
||||
A recent and currently supported version of Python (right now, <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.
|
||||
|
||||
@ -68,7 +68,7 @@ Successfully installed sqlmodel
|
||||
|
||||
## 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. ✨
|
||||
|
||||
|
@ -2,13 +2,81 @@
|
||||
|
||||
## 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).
|
||||
* ⬆ 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).
|
||||
* ⬆ 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 small typos in docs. PR [#481](https://github.com/tiangolo/sqlmodel/pull/481) by [@micuffaro](https://github.com/micuffaro).
|
||||
|
||||
### Internal
|
||||
|
||||
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#672](https://github.com/tiangolo/sqlmodel/pull/672) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
|
||||
* ⬆ Bump dawidd6/action-download-artifact from 2.24.2 to 2.28.0. PR [#660](https://github.com/tiangolo/sqlmodel/pull/660) by [@dependabot[bot]](https://github.com/apps/dependabot).
|
||||
* ✅ Refactor OpenAPI FastAPI tests to simplify updating them later, this moves things around without changes. PR [#671](https://github.com/tiangolo/sqlmodel/pull/671) by [@tiangolo](https://github.com/tiangolo).
|
||||
* ⬆ Bump actions/checkout from 3 to 4. PR [#670](https://github.com/tiangolo/sqlmodel/pull/670) by [@dependabot[bot]](https://github.com/apps/dependabot).
|
||||
* 🔧 Update mypy config, use `strict = true` instead of manual configs. PR [#428](https://github.com/tiangolo/sqlmodel/pull/428) by [@michaeloliverx](https://github.com/michaeloliverx).
|
||||
* ⬆️ Upgrade MkDocs Material. PR [#668](https://github.com/tiangolo/sqlmodel/pull/668) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 🎨 Update docs format and references with pre-commit and Ruff. PR [#667](https://github.com/tiangolo/sqlmodel/pull/667) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 🎨 Run pre-commit on all files and autoformat. PR [#666](https://github.com/tiangolo/sqlmodel/pull/666) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 👷 Move to Ruff and add pre-commit. PR [#661](https://github.com/tiangolo/sqlmodel/pull/661) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 🛠️ Add `CITATION.cff` file for academic citations. PR [#13](https://github.com/tiangolo/sqlmodel/pull/13) by [@sugatoray](https://github.com/sugatoray).
|
||||
* 👷 Update docs deployments to Cloudflare. PR [#630](https://github.com/tiangolo/sqlmodel/pull/630) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 👷♂️ Upgrade CI for docs. PR [#628](https://github.com/tiangolo/sqlmodel/pull/628) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 👷 Update CI debug mode with Tmate. PR [#629](https://github.com/tiangolo/sqlmodel/pull/629) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 👷 Update latest changes token. PR [#616](https://github.com/tiangolo/sqlmodel/pull/616) by [@tiangolo](https://github.com/tiangolo).
|
||||
* ⬆️ Upgrade analytics. PR [#558](https://github.com/tiangolo/sqlmodel/pull/558) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 🔧 Update new issue chooser to point to GitHub Discussions. PR [#546](https://github.com/tiangolo/sqlmodel/pull/546) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 🔧 Add template for GitHub Discussion questions and update issues template. PR [#544](https://github.com/tiangolo/sqlmodel/pull/544) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 👷 Refactor CI artifact upload/download for docs previews. PR [#514](https://github.com/tiangolo/sqlmodel/pull/514) by [@tiangolo](https://github.com/tiangolo).
|
||||
* ⬆ Bump actions/cache from 2 to 3. PR [#497](https://github.com/tiangolo/sqlmodel/pull/497) by [@dependabot[bot]](https://github.com/apps/dependabot).
|
||||
* ⬆ Bump dawidd6/action-download-artifact from 2.24.0 to 2.24.2. PR [#493](https://github.com/tiangolo/sqlmodel/pull/493) by [@dependabot[bot]](https://github.com/apps/dependabot).
|
||||
* 🔧 Update Smokeshow coverage threshold. PR [#487](https://github.com/tiangolo/sqlmodel/pull/487) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 👷 Move from Codecov to Smokeshow. PR [#486](https://github.com/tiangolo/sqlmodel/pull/486) by [@tiangolo](https://github.com/tiangolo).
|
||||
* ⬆ Bump actions/setup-python from 2 to 4. PR [#411](https://github.com/tiangolo/sqlmodel/pull/411) by [@dependabot[bot]](https://github.com/apps/dependabot).
|
||||
@ -28,7 +96,7 @@
|
||||
|
||||
### 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).
|
||||
|
||||
### Docs
|
||||
|
@ -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]!}
|
||||
|
||||
# Code below ommitted 👇
|
||||
# Code below omitted 👇
|
||||
```
|
||||
|
||||
<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]!}
|
||||
|
||||
# Code below ommitted 👇
|
||||
# Code below omitted 👇
|
||||
```
|
||||
|
||||
<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]!}
|
||||
|
||||
# Code below ommitted 👇
|
||||
# Code below omitted 👇
|
||||
```
|
||||
|
||||
<details>
|
||||
@ -271,7 +271,7 @@ Let's see how it works:
|
||||
```console
|
||||
$ python app.py
|
||||
|
||||
// Output above ommitted 👆
|
||||
// Output above omitted 👆
|
||||
|
||||
// After committing, the objects are expired and have no values
|
||||
After committing the session
|
||||
@ -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]!}
|
||||
|
||||
# Code below ommitted 👇
|
||||
# Code below omitted 👇
|
||||
```
|
||||
|
||||
<details>
|
||||
@ -362,7 +362,7 @@ Here's how the output would look like:
|
||||
```console
|
||||
$ python app.py
|
||||
|
||||
// Output above ommitted 👆
|
||||
// Output above omitted 👆
|
||||
|
||||
// The first refresh
|
||||
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age
|
||||
@ -427,7 +427,7 @@ And the output shows again the same data:
|
||||
```console
|
||||
$ python app.py
|
||||
|
||||
// Output above ommitted 👆
|
||||
// Output above omitted 👆
|
||||
|
||||
// By finishing the with block, the Session is closed, including a rollback of any pending transaction that could have been there and was not committed
|
||||
INFO Engine ROLLBACK
|
||||
|
@ -168,7 +168,7 @@ Let's assume that now the file structure is:
|
||||
|
||||
### Circular Imports and Type Annotations
|
||||
|
||||
The problem with circular imports is that Python can't resolve them at <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.
|
||||
|
||||
|
@ -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:
|
||||
|
||||
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.
|
||||
|
||||
|
@ -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 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. 🔍
|
||||
|
@ -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:///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://`
|
||||
|
||||
|
@ -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`! 😱
|
||||
|
||||
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.
|
||||
|
||||
|
@ -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.
|
||||
|
||||
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**.
|
||||
|
||||
|
@ -84,7 +84,7 @@ In this case, we used `response_model=TeamRead` and `response_model=HeroRead`, s
|
||||
|
||||
# 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 👇
|
||||
```
|
||||
@ -234,7 +234,7 @@ In the case of the hero, this tells FastAPI to extract the `team` too. And in th
|
||||
|
||||
# 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 👇
|
||||
```
|
||||
|
@ -177,7 +177,7 @@ And then we remove the previous `with` block with the old **session**.
|
||||
|
||||
# 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>
|
||||
|
@ -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"
|
||||
# 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 👇
|
||||
```
|
||||
|
@ -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.
|
||||
|
||||
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.
|
||||
|
||||
|
@ -64,15 +64,13 @@ $ cd sqlmodel-tutorial
|
||||
|
||||
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:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ python3 --version
|
||||
Python 3.6.9
|
||||
Python 3.11
|
||||
```
|
||||
|
||||
</div>
|
||||
@ -84,8 +82,6 @@ You might want to try with the specific versions, for example with:
|
||||
* `python3.10`
|
||||
* `python3.9`
|
||||
* `python3.8`
|
||||
* `python3.7`
|
||||
* `python3.6`
|
||||
|
||||
The code would look like this:
|
||||
|
||||
@ -139,7 +135,7 @@ Here are the commands you could use:
|
||||
// Remember that you might need to use python3.9 or similar 💡
|
||||
// Create the virtual environment using the module "venv"
|
||||
$ python3 -m venv env
|
||||
// ...here it creates the virtual enviroment in the directory "env"
|
||||
// ...here it creates the virtual environment in the directory "env"
|
||||
// Activate the virtual environment
|
||||
$ source ./env/bin/activate
|
||||
// Verify that the virtual environment is active
|
||||
@ -161,7 +157,7 @@ Here are the commands you could use:
|
||||
```console
|
||||
// Create the virtual environment using the module "venv"
|
||||
# >$ 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
|
||||
# >$ .\env\Scripts\Activate.ps1
|
||||
// Verify that the virtual environment is active
|
||||
|
@ -171,7 +171,7 @@ The first step is to import the `Session` class:
|
||||
```Python hl_lines="3"
|
||||
{!./docs_src/tutorial/insert/tutorial001.py[ln:1-3]!}
|
||||
|
||||
# Code below ommitted 👇
|
||||
# Code below omitted 👇
|
||||
```
|
||||
|
||||
<details>
|
||||
|
@ -271,11 +271,11 @@ Of course, you can also combine `.limit()` and `.offset()` with `.where()` and o
|
||||
|
||||
</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.
|
||||
|
||||
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">
|
||||
|
||||
@ -284,18 +284,17 @@ $ python app.py
|
||||
|
||||
// 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
|
||||
FROM hero
|
||||
WHERE hero.age > ?
|
||||
LIMIT ? OFFSET ?
|
||||
INFO Engine [no key 0.00022s] (32, 3, 0)
|
||||
INFO Engine [no key 0.00022s] (32, 2, 1)
|
||||
|
||||
// Print the heroes received, only 3
|
||||
// Print the heroes received, only 2
|
||||
[
|
||||
Hero(age=35, secret_name='Trevor Challa', id=5, name='Black Lion'),
|
||||
Hero(age=36, secret_name='Steve Weird', id=6, name='Dr. Weird'),
|
||||
Hero(age=48, secret_name='Tommy Sharp', id=3, name='Rusty-Man')
|
||||
Hero(age=36, id=6, name='Dr. Weird', secret_name='Steve Weird'),
|
||||
Hero(age=48, id=3, name='Rusty-Man', secret_name='Tommy Sharp')
|
||||
]
|
||||
```
|
||||
|
||||
|
@ -179,4 +179,4 @@ INFO Engine ROLLBACK
|
||||
|
||||
## Recap
|
||||
|
||||
After setting up the model link, using it with **relationship attributes** is fairly straighforward, just Python objects. ✨
|
||||
After setting up the model link, using it with **relationship attributes** is fairly straightforward, just Python objects. ✨
|
||||
|
@ -12,7 +12,7 @@ Let's see the utilities to read a single row.
|
||||
|
||||
## Continue From Previous Code
|
||||
|
||||
We'll continue with the same examples we have been using in the previous chapters to create and select data and we'll keep udpating them.
|
||||
We'll continue with the same examples we have been using in the previous chapters to create and select data and we'll keep updating them.
|
||||
|
||||
<details>
|
||||
<summary>👀 Full file preview</summary>
|
||||
|
@ -123,7 +123,7 @@ Now let's update **Spider-Boy**, removing him from the team by setting `hero_spi
|
||||
|
||||
</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**. 🔥
|
||||
|
||||
|
@ -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).
|
||||
|
||||
If it was required for a `Hero` instance to belong to a `Team`, then the `team_id` would be `int` instead of `Optional[int]`.
|
||||
|
||||
And the `team` attribute would be a `Team` instead of `Optional[Team]`.
|
||||
If it was required for a `Hero` instance to belong to a `Team`, then the `team_id` would be `int` instead of `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]`.
|
||||
|
||||
## Relationship Attributes With Lists
|
||||
|
||||
|
@ -52,7 +52,7 @@ With what we have learned **up to now**, we could use a `select()` statement, th
|
||||
|
||||
## 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:
|
||||
|
||||
|
@ -190,7 +190,7 @@ First we have to import `select` from `sqlmodel` at the top of the file:
|
||||
```Python hl_lines="3"
|
||||
{!./docs_src/tutorial/select/tutorial001.py[ln:1-3]!}
|
||||
|
||||
# More code below ommitted 👇
|
||||
# More code below omitted 👇
|
||||
```
|
||||
|
||||
<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()`.
|
||||
|
||||
**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. ✨
|
||||
|
||||
|
@ -206,7 +206,7 @@ We care specially about the **select** statement:
|
||||
|
||||
## Filter Rows Using `WHERE` with **SQLModel**
|
||||
|
||||
Now, the same way that we add `WHERE` to a SQL statement to filter rows, we can add a `.where()` to a **SQLModel** `select()` statment to filter rows, which will filter the objects returned:
|
||||
Now, the same way that we add `WHERE` to a SQL statement to filter rows, we can add a `.where()` to a **SQLModel** `select()` statement to filter rows, which will filter the objects returned:
|
||||
|
||||
```Python hl_lines="5"
|
||||
# Code above omitted 👆
|
||||
@ -748,7 +748,7 @@ FROM hero
|
||||
WHERE hero.age >= ? AND hero.age < ?
|
||||
INFO Engine [no key 0.00014s] (35, 40)
|
||||
|
||||
// The two heros printed
|
||||
// The two heroes printed
|
||||
age=35 id=5 name='Black Lion' secret_name='Trevor Challa'
|
||||
age=36 id=6 name='Dr. Weird' secret_name='Steve Weird'
|
||||
|
||||
|
@ -157,7 +157,7 @@
|
||||
Hero 3:
|
||||
```
|
||||
|
||||
21. Print the line `"After commiting the session, show IDs"`.
|
||||
21. Print the line `"After committing the session, show IDs"`.
|
||||
|
||||
Generates the output:
|
||||
|
||||
|
@ -21,56 +21,56 @@ def create_db_and_tables():
|
||||
|
||||
|
||||
def create_heroes():
|
||||
hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") # (1)
|
||||
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") # (2)
|
||||
hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) # (3)
|
||||
hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") # (1)!
|
||||
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") # (2)!
|
||||
hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) # (3)!
|
||||
|
||||
print("Before interacting with the database") # (4)
|
||||
print("Hero 1:", hero_1) # (5)
|
||||
print("Hero 2:", hero_2) # (6)
|
||||
print("Hero 3:", hero_3) # (7)
|
||||
print("Before interacting with the database") # (4)!
|
||||
print("Hero 1:", hero_1) # (5)!
|
||||
print("Hero 2:", hero_2) # (6)!
|
||||
print("Hero 3:", hero_3) # (7)!
|
||||
|
||||
with Session(engine) as session: # (8)
|
||||
session.add(hero_1) # (9)
|
||||
session.add(hero_2) # (10)
|
||||
session.add(hero_3) # (11)
|
||||
with Session(engine) as session: # (8)!
|
||||
session.add(hero_1) # (9)!
|
||||
session.add(hero_2) # (10)!
|
||||
session.add(hero_3) # (11)!
|
||||
|
||||
print("After adding to the session") # (12)
|
||||
print("Hero 1:", hero_1) # (13)
|
||||
print("Hero 2:", hero_2) # (14)
|
||||
print("Hero 3:", hero_3) # (15)
|
||||
print("After adding to the session") # (12)!
|
||||
print("Hero 1:", hero_1) # (13)!
|
||||
print("Hero 2:", hero_2) # (14)!
|
||||
print("Hero 3:", hero_3) # (15)!
|
||||
|
||||
session.commit() # (16)
|
||||
session.commit() # (16)!
|
||||
|
||||
print("After committing the session") # (17)
|
||||
print("Hero 1:", hero_1) # (18)
|
||||
print("Hero 2:", hero_2) # (19)
|
||||
print("Hero 3:", hero_3) # (20)
|
||||
print("After committing the session") # (17)!
|
||||
print("Hero 1:", hero_1) # (18)!
|
||||
print("Hero 2:", hero_2) # (19)!
|
||||
print("Hero 3:", hero_3) # (20)!
|
||||
|
||||
print("After committing the session, show IDs") # (21)
|
||||
print("Hero 1 ID:", hero_1.id) # (22)
|
||||
print("Hero 2 ID:", hero_2.id) # (23)
|
||||
print("Hero 3 ID:", hero_3.id) # (24)
|
||||
print("After committing the session, show IDs") # (21)!
|
||||
print("Hero 1 ID:", hero_1.id) # (22)!
|
||||
print("Hero 2 ID:", hero_2.id) # (23)!
|
||||
print("Hero 3 ID:", hero_3.id) # (24)!
|
||||
|
||||
print("After committing the session, show names") # (25)
|
||||
print("Hero 1 name:", hero_1.name) # (26)
|
||||
print("Hero 2 name:", hero_2.name) # (27)
|
||||
print("Hero 3 name:", hero_3.name) # (28)
|
||||
print("After committing the session, show names") # (25)!
|
||||
print("Hero 1 name:", hero_1.name) # (26)!
|
||||
print("Hero 2 name:", hero_2.name) # (27)!
|
||||
print("Hero 3 name:", hero_3.name) # (28)!
|
||||
|
||||
session.refresh(hero_1) # (29)
|
||||
session.refresh(hero_2) # (30)
|
||||
session.refresh(hero_3) # (31)
|
||||
session.refresh(hero_1) # (29)!
|
||||
session.refresh(hero_2) # (30)!
|
||||
session.refresh(hero_3) # (31)!
|
||||
|
||||
print("After refreshing the heroes") # (32)
|
||||
print("Hero 1:", hero_1) # (33)
|
||||
print("Hero 2:", hero_2) # (34)
|
||||
print("Hero 3:", hero_3) # (35)
|
||||
# (36)
|
||||
print("After refreshing the heroes") # (32)!
|
||||
print("Hero 1:", hero_1) # (33)!
|
||||
print("Hero 2:", hero_2) # (34)!
|
||||
print("Hero 3:", hero_3) # (35)!
|
||||
# (36)!
|
||||
|
||||
print("After the session closes") # (37)
|
||||
print("Hero 1:", hero_1) # (38)
|
||||
print("Hero 2:", hero_2) # (39)
|
||||
print("Hero 3:", hero_3) # (40)
|
||||
print("After the session closes") # (37)!
|
||||
print("Hero 1:", hero_1) # (38)!
|
||||
print("Hero 2:", hero_2) # (39)!
|
||||
print("Hero 3:", hero_3) # (40)!
|
||||
|
||||
|
||||
def main():
|
||||
|
@ -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)
|
||||
id: Optional[int] = Field(default=None, primary_key=True) # (4)
|
||||
name: str # (5)
|
||||
secret_name: str # (6)
|
||||
age: Optional[int] = None # (7)
|
||||
class Hero(SQLModel, table=True): # (3)!
|
||||
id: Optional[int] = Field(default=None, primary_key=True) # (4)!
|
||||
name: str # (5)!
|
||||
secret_name: str # (6)!
|
||||
age: Optional[int] = None # (7)!
|
||||
|
||||
|
||||
sqlite_file_name = "database.db" # (8)
|
||||
sqlite_url = f"sqlite:///{sqlite_file_name}" # (9)
|
||||
sqlite_file_name = "database.db" # (8)!
|
||||
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)
|
||||
SQLModel.metadata.create_all(engine) # (12)
|
||||
def create_db_and_tables(): # (11)!
|
||||
SQLModel.metadata.create_all(engine) # (12)!
|
||||
|
||||
|
||||
if __name__ == "__main__": # (13)
|
||||
create_db_and_tables() # (14)
|
||||
if __name__ == "__main__": # (13)!
|
||||
create_db_and_tables() # (14)!
|
||||
|
@ -71,23 +71,23 @@ def update_heroes():
|
||||
|
||||
def delete_heroes():
|
||||
with Session(engine) as session:
|
||||
statement = select(Hero).where(Hero.name == "Spider-Youngster") # (1)
|
||||
results = session.exec(statement) # (2)
|
||||
hero = results.one() # (3)
|
||||
print("Hero: ", hero) # (4)
|
||||
statement = select(Hero).where(Hero.name == "Spider-Youngster") # (1)!
|
||||
results = session.exec(statement) # (2)!
|
||||
hero = results.one() # (3)!
|
||||
print("Hero: ", hero) # (4)!
|
||||
|
||||
session.delete(hero) # (5)
|
||||
session.commit() # (6)
|
||||
session.delete(hero) # (5)!
|
||||
session.commit() # (6)!
|
||||
|
||||
print("Deleted hero:", hero) # (7)
|
||||
print("Deleted hero:", hero) # (7)!
|
||||
|
||||
statement = select(Hero).where(Hero.name == "Spider-Youngster") # (8)
|
||||
results = session.exec(statement) # (9)
|
||||
hero = results.first() # (10)
|
||||
statement = select(Hero).where(Hero.name == "Spider-Youngster") # (8)!
|
||||
results = session.exec(statement) # (9)!
|
||||
hero = results.first() # (10)!
|
||||
|
||||
if hero is None: # (11)
|
||||
print("There's no hero named Spider-Youngster") # (12)
|
||||
# (13)
|
||||
if hero is None: # (11)!
|
||||
print("There's no hero named Spider-Youngster") # (12)!
|
||||
# (13)!
|
||||
|
||||
|
||||
def main():
|
||||
|
@ -66,7 +66,7 @@ def read_heroes(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
offset: int = 0,
|
||||
limit: int = Query(default=100, lte=100),
|
||||
limit: int = Query(default=100, le=100),
|
||||
):
|
||||
heroes = session.exec(select(Hero).offset(offset).limit(limit)).all()
|
||||
return heroes
|
||||
@ -98,7 +98,6 @@ def update_hero(
|
||||
|
||||
@app.delete("/heroes/{hero_id}")
|
||||
def delete_hero(*, session: Session = Depends(get_session), hero_id: int):
|
||||
|
||||
hero = session.get(Hero, hero_id)
|
||||
if not hero:
|
||||
raise HTTPException(status_code=404, detail="Hero not found")
|
||||
|
@ -1,7 +1,7 @@
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlmodel import Session, SQLModel, create_engine
|
||||
|
||||
from .main import app, get_session # (1)
|
||||
from .main import app, get_session # (1)!
|
||||
|
||||
|
||||
def test_create_hero():
|
||||
@ -17,16 +17,16 @@ def test_create_hero():
|
||||
|
||||
app.dependency_overrides[get_session] = get_session_override
|
||||
|
||||
client = TestClient(app) # (2)
|
||||
client = TestClient(app) # (2)!
|
||||
|
||||
response = client.post( # (3)
|
||||
response = client.post( # (3)!
|
||||
"/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"}
|
||||
)
|
||||
app.dependency_overrides.clear()
|
||||
data = response.json() # (4)
|
||||
data = response.json() # (4)!
|
||||
|
||||
assert response.status_code == 200 # (5)
|
||||
assert data["name"] == "Deadpond" # (6)
|
||||
assert data["secret_name"] == "Dive Wilson" # (7)
|
||||
assert data["age"] is None # (8)
|
||||
assert data["id"] is not None # (9)
|
||||
assert response.status_code == 200 # (5)!
|
||||
assert data["name"] == "Deadpond" # (6)!
|
||||
assert data["secret_name"] == "Dive Wilson" # (7)!
|
||||
assert data["age"] is None # (8)!
|
||||
assert data["id"] is not None # (9)!
|
||||
|
@ -1,7 +1,7 @@
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlmodel import Session, SQLModel, create_engine
|
||||
|
||||
from .main import app, get_session # (1)
|
||||
from .main import app, get_session # (1)!
|
||||
|
||||
|
||||
def test_create_hero():
|
||||
@ -12,17 +12,17 @@ def test_create_hero():
|
||||
|
||||
with Session(engine) as session:
|
||||
|
||||
def get_session_override(): # (2)
|
||||
return session # (3)
|
||||
def get_session_override(): # (2)!
|
||||
return session # (3)!
|
||||
|
||||
app.dependency_overrides[get_session] = get_session_override # (4)
|
||||
app.dependency_overrides[get_session] = get_session_override # (4)!
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.post(
|
||||
"/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"}
|
||||
)
|
||||
app.dependency_overrides.clear() # (5)
|
||||
app.dependency_overrides.clear() # (5)!
|
||||
data = response.json()
|
||||
|
||||
assert response.status_code == 200
|
||||
|
@ -1,21 +1,21 @@
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlmodel import Session, SQLModel, create_engine
|
||||
|
||||
from .main import app, get_session # (1)
|
||||
from .main import app, get_session # (1)!
|
||||
|
||||
|
||||
def test_create_hero():
|
||||
engine = create_engine( # (2)
|
||||
engine = create_engine( # (2)!
|
||||
"sqlite:///testing.db", connect_args={"check_same_thread": False}
|
||||
)
|
||||
SQLModel.metadata.create_all(engine) # (3)
|
||||
SQLModel.metadata.create_all(engine) # (3)!
|
||||
|
||||
with Session(engine) as session: # (4)
|
||||
with Session(engine) as session: # (4)!
|
||||
|
||||
def get_session_override():
|
||||
return session # (5)
|
||||
return session # (5)!
|
||||
|
||||
app.dependency_overrides[get_session] = get_session_override # (4)
|
||||
app.dependency_overrides[get_session] = get_session_override # (4)!
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
@ -30,4 +30,4 @@ def test_create_hero():
|
||||
assert data["secret_name"] == "Dive Wilson"
|
||||
assert data["age"] is None
|
||||
assert data["id"] is not None
|
||||
# (6)
|
||||
# (6)!
|
||||
|
@ -1,15 +1,15 @@
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlmodel import Session, SQLModel, create_engine
|
||||
from sqlmodel.pool import StaticPool # (1)
|
||||
from sqlmodel.pool import StaticPool # (1)!
|
||||
|
||||
from .main import app, get_session
|
||||
|
||||
|
||||
def test_create_hero():
|
||||
engine = create_engine(
|
||||
"sqlite://", # (2)
|
||||
"sqlite://", # (2)!
|
||||
connect_args={"check_same_thread": False},
|
||||
poolclass=StaticPool, # (3)
|
||||
poolclass=StaticPool, # (3)!
|
||||
)
|
||||
SQLModel.metadata.create_all(engine)
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import pytest # (1)
|
||||
import pytest # (1)!
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlmodel import Session, SQLModel, create_engine
|
||||
from sqlmodel.pool import StaticPool
|
||||
@ -6,19 +6,19 @@ from sqlmodel.pool import StaticPool
|
||||
from .main import app, get_session
|
||||
|
||||
|
||||
@pytest.fixture(name="session") # (2)
|
||||
def session_fixture(): # (3)
|
||||
@pytest.fixture(name="session") # (2)!
|
||||
def session_fixture(): # (3)!
|
||||
engine = create_engine(
|
||||
"sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool
|
||||
)
|
||||
SQLModel.metadata.create_all(engine)
|
||||
with Session(engine) as session:
|
||||
yield session # (4)
|
||||
yield session # (4)!
|
||||
|
||||
|
||||
def test_create_hero(session: Session): # (5)
|
||||
def test_create_hero(session: Session): # (5)!
|
||||
def get_session_override():
|
||||
return session # (6)
|
||||
return session # (6)!
|
||||
|
||||
app.dependency_overrides[get_session] = get_session_override
|
||||
|
||||
|
@ -16,19 +16,19 @@ def session_fixture():
|
||||
yield session
|
||||
|
||||
|
||||
@pytest.fixture(name="client") # (1)
|
||||
def client_fixture(session: Session): # (2)
|
||||
def get_session_override(): # (3)
|
||||
@pytest.fixture(name="client") # (1)!
|
||||
def client_fixture(session: Session): # (2)!
|
||||
def get_session_override(): # (3)!
|
||||
return session
|
||||
|
||||
app.dependency_overrides[get_session] = get_session_override # (4)
|
||||
app.dependency_overrides[get_session] = get_session_override # (4)!
|
||||
|
||||
client = TestClient(app) # (5)
|
||||
yield client # (6)
|
||||
app.dependency_overrides.clear() # (7)
|
||||
client = TestClient(app) # (5)!
|
||||
yield client # (6)!
|
||||
app.dependency_overrides.clear() # (7)!
|
||||
|
||||
|
||||
def test_create_hero(client: TestClient): # (8)
|
||||
def test_create_hero(client: TestClient): # (8)!
|
||||
response = client.post(
|
||||
"/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"}
|
||||
)
|
||||
|
@ -58,7 +58,7 @@ def create_hero(hero: HeroCreate):
|
||||
|
||||
|
||||
@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:
|
||||
heroes = session.exec(select(Hero).offset(offset).limit(limit)).all()
|
||||
return heroes
|
||||
|
@ -52,7 +52,7 @@ def create_hero(hero: HeroCreate):
|
||||
|
||||
|
||||
@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:
|
||||
heroes = session.exec(select(Hero).offset(offset).limit(limit)).all()
|
||||
return heroes
|
||||
|
@ -104,7 +104,7 @@ def read_heroes(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
offset: int = 0,
|
||||
limit: int = Query(default=100, lte=100),
|
||||
limit: int = Query(default=100, le=100),
|
||||
):
|
||||
heroes = session.exec(select(Hero).offset(offset).limit(limit)).all()
|
||||
return heroes
|
||||
@ -136,7 +136,6 @@ def update_hero(
|
||||
|
||||
@app.delete("/heroes/{hero_id}")
|
||||
def delete_hero(*, session: Session = Depends(get_session), hero_id: int):
|
||||
|
||||
hero = session.get(Hero, hero_id)
|
||||
if not hero:
|
||||
raise HTTPException(status_code=404, detail="Hero not found")
|
||||
@ -159,7 +158,7 @@ def read_teams(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
offset: int = 0,
|
||||
limit: int = Query(default=100, lte=100),
|
||||
limit: int = Query(default=100, le=100),
|
||||
):
|
||||
teams = session.exec(select(Team).offset(offset).limit(limit)).all()
|
||||
return teams
|
||||
|
@ -66,7 +66,7 @@ def read_heroes(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
offset: int = 0,
|
||||
limit: int = Query(default=100, lte=100),
|
||||
limit: int = Query(default=100, le=100),
|
||||
):
|
||||
heroes = session.exec(select(Hero).offset(offset).limit(limit)).all()
|
||||
return heroes
|
||||
@ -98,7 +98,6 @@ def update_hero(
|
||||
|
||||
@app.delete("/heroes/{hero_id}")
|
||||
def delete_hero(*, session: Session = Depends(get_session), hero_id: int):
|
||||
|
||||
hero = session.get(Hero, hero_id)
|
||||
if not hero:
|
||||
raise HTTPException(status_code=404, detail="Hero not found")
|
||||
|
@ -95,7 +95,7 @@ def read_heroes(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
offset: int = 0,
|
||||
limit: int = Query(default=100, lte=100),
|
||||
limit: int = Query(default=100, le=100),
|
||||
):
|
||||
heroes = session.exec(select(Hero).offset(offset).limit(limit)).all()
|
||||
return heroes
|
||||
@ -127,7 +127,6 @@ def update_hero(
|
||||
|
||||
@app.delete("/heroes/{hero_id}")
|
||||
def delete_hero(*, session: Session = Depends(get_session), hero_id: int):
|
||||
|
||||
hero = session.get(Hero, hero_id)
|
||||
if not hero:
|
||||
raise HTTPException(status_code=404, detail="Hero not found")
|
||||
@ -150,7 +149,7 @@ def read_teams(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
offset: int = 0,
|
||||
limit: int = Query(default=100, lte=100),
|
||||
limit: int = Query(default=100, le=100),
|
||||
):
|
||||
teams = session.exec(select(Team).offset(offset).limit(limit)).all()
|
||||
return teams
|
||||
|
@ -58,7 +58,7 @@ def create_hero(hero: HeroCreate):
|
||||
|
||||
|
||||
@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:
|
||||
heroes = session.exec(select(Hero).offset(offset).limit(limit)).all()
|
||||
return heroes
|
||||
|
@ -20,24 +20,24 @@ def create_db_and_tables():
|
||||
SQLModel.metadata.create_all(engine)
|
||||
|
||||
|
||||
def create_heroes(): # (1)
|
||||
hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") # (2)
|
||||
def create_heroes(): # (1)!
|
||||
hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") # (2)!
|
||||
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
|
||||
hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48)
|
||||
|
||||
with Session(engine) as session: # (3)
|
||||
session.add(hero_1) # (4)
|
||||
with Session(engine) as session: # (3)!
|
||||
session.add(hero_1) # (4)!
|
||||
session.add(hero_2)
|
||||
session.add(hero_3)
|
||||
|
||||
session.commit() # (5)
|
||||
# (6)
|
||||
session.commit() # (5)!
|
||||
# (6)!
|
||||
|
||||
|
||||
def main(): # (7)
|
||||
create_db_and_tables() # (8)
|
||||
create_heroes() # (9)
|
||||
def main(): # (7)!
|
||||
create_db_and_tables() # (8)!
|
||||
create_heroes() # (9)!
|
||||
|
||||
|
||||
if __name__ == "__main__": # (10)
|
||||
main() # (11)
|
||||
if __name__ == "__main__": # (10)!
|
||||
main() # (11)!
|
||||
|
@ -43,7 +43,7 @@ def create_heroes():
|
||||
|
||||
def select_heroes():
|
||||
with Session(engine) as session:
|
||||
statement = select(Hero).where(Hero.age > 32).limit(3)
|
||||
statement = select(Hero).where(Hero.age > 32).offset(1).limit(2)
|
||||
results = session.exec(statement)
|
||||
heroes = results.all()
|
||||
print(heroes)
|
||||
|
@ -1,9 +1,9 @@
|
||||
from typing import Optional
|
||||
|
||||
from sqlmodel import Field, Session, SQLModel, create_engine, select # (1)
|
||||
from sqlmodel import Field, Session, SQLModel, create_engine, select # (1)!
|
||||
|
||||
|
||||
class Hero(SQLModel, table=True): # (2)
|
||||
class Hero(SQLModel, table=True): # (2)!
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
name: str
|
||||
secret_name: str
|
||||
@ -13,19 +13,19 @@ class Hero(SQLModel, table=True): # (2)
|
||||
sqlite_file_name = "database.db"
|
||||
sqlite_url = f"sqlite:///{sqlite_file_name}"
|
||||
|
||||
engine = create_engine(sqlite_url, echo=True) # (3)
|
||||
engine = create_engine(sqlite_url, echo=True) # (3)!
|
||||
|
||||
|
||||
def create_db_and_tables():
|
||||
SQLModel.metadata.create_all(engine) # (4)
|
||||
SQLModel.metadata.create_all(engine) # (4)!
|
||||
|
||||
|
||||
def create_heroes():
|
||||
hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") # (5)
|
||||
hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") # (5)!
|
||||
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
|
||||
hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48)
|
||||
|
||||
with Session(engine) as session: # (6)
|
||||
with Session(engine) as session: # (6)!
|
||||
session.add(hero_1)
|
||||
session.add(hero_2)
|
||||
session.add(hero_3)
|
||||
@ -34,18 +34,18 @@ def create_heroes():
|
||||
|
||||
|
||||
def select_heroes():
|
||||
with Session(engine) as session: # (7)
|
||||
statement = select(Hero) # (8)
|
||||
results = session.exec(statement) # (9)
|
||||
for hero in results: # (10)
|
||||
print(hero) # (11)
|
||||
# (12)
|
||||
with Session(engine) as session: # (7)!
|
||||
statement = select(Hero) # (8)!
|
||||
results = session.exec(statement) # (9)!
|
||||
for hero in results: # (10)!
|
||||
print(hero) # (11)!
|
||||
# (12)!
|
||||
|
||||
|
||||
def main():
|
||||
create_db_and_tables()
|
||||
create_heroes()
|
||||
select_heroes() # (13)
|
||||
select_heroes() # (13)!
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
@ -132,7 +132,7 @@
|
||||
!!! tip
|
||||
SQLAlchemy is still using the previous transaction, so it doesn't have to create a new one.
|
||||
|
||||
18. Print the first hero, now udpated.
|
||||
18. Print the first hero, now updated.
|
||||
|
||||
This generates the output:
|
||||
|
||||
|
@ -43,16 +43,16 @@ def create_heroes():
|
||||
|
||||
def update_heroes():
|
||||
with Session(engine) as session:
|
||||
statement = select(Hero).where(Hero.name == "Spider-Boy") # (1)
|
||||
results = session.exec(statement) # (2)
|
||||
hero = results.one() # (3)
|
||||
print("Hero:", hero) # (4)
|
||||
statement = select(Hero).where(Hero.name == "Spider-Boy") # (1)!
|
||||
results = session.exec(statement) # (2)!
|
||||
hero = results.one() # (3)!
|
||||
print("Hero:", hero) # (4)!
|
||||
|
||||
hero.age = 16 # (5)
|
||||
session.add(hero) # (6)
|
||||
session.commit() # (7)
|
||||
session.refresh(hero) # (8)
|
||||
print("Updated hero:", hero) # (9)
|
||||
hero.age = 16 # (5)!
|
||||
session.add(hero) # (6)!
|
||||
session.commit() # (7)!
|
||||
session.refresh(hero) # (8)!
|
||||
print("Updated hero:", hero) # (9)!
|
||||
|
||||
|
||||
def main():
|
||||
|
@ -43,31 +43,31 @@ def create_heroes():
|
||||
|
||||
def update_heroes():
|
||||
with Session(engine) as session:
|
||||
statement = select(Hero).where(Hero.name == "Spider-Boy") # (1)
|
||||
results = session.exec(statement) # (2)
|
||||
hero_1 = results.one() # (3)
|
||||
print("Hero 1:", hero_1) # (4)
|
||||
statement = select(Hero).where(Hero.name == "Spider-Boy") # (1)!
|
||||
results = session.exec(statement) # (2)!
|
||||
hero_1 = results.one() # (3)!
|
||||
print("Hero 1:", hero_1) # (4)!
|
||||
|
||||
statement = select(Hero).where(Hero.name == "Captain North America") # (5)
|
||||
results = session.exec(statement) # (6)
|
||||
hero_2 = results.one() # (7)
|
||||
print("Hero 2:", hero_2) # (8)
|
||||
statement = select(Hero).where(Hero.name == "Captain North America") # (5)!
|
||||
results = session.exec(statement) # (6)!
|
||||
hero_2 = results.one() # (7)!
|
||||
print("Hero 2:", hero_2) # (8)!
|
||||
|
||||
hero_1.age = 16 # (9)
|
||||
hero_1.name = "Spider-Youngster" # (10)
|
||||
session.add(hero_1) # (11)
|
||||
hero_1.age = 16 # (9)!
|
||||
hero_1.name = "Spider-Youngster" # (10)!
|
||||
session.add(hero_1) # (11)!
|
||||
|
||||
hero_2.name = "Captain North America Except Canada" # (12)
|
||||
hero_2.age = 110 # (13)
|
||||
session.add(hero_2) # (14)
|
||||
hero_2.name = "Captain North America Except Canada" # (12)!
|
||||
hero_2.age = 110 # (13)!
|
||||
session.add(hero_2) # (14)!
|
||||
|
||||
session.commit() # (15)
|
||||
session.refresh(hero_1) # (16)
|
||||
session.refresh(hero_2) # (17)
|
||||
session.commit() # (15)!
|
||||
session.refresh(hero_1) # (16)!
|
||||
session.refresh(hero_2) # (17)!
|
||||
|
||||
print("Updated hero 1:", hero_1) # (18)
|
||||
print("Updated hero 2:", hero_2) # (19)
|
||||
# (20)
|
||||
print("Updated hero 1:", hero_1) # (18)!
|
||||
print("Updated hero 2:", hero_2) # (19)!
|
||||
# (20)!
|
||||
|
||||
|
||||
def main():
|
||||
|
@ -110,7 +110,7 @@ markdown_extensions:
|
||||
extra:
|
||||
analytics:
|
||||
provider: google
|
||||
property: UA-205713594-2
|
||||
property: G-J8HVTT936W
|
||||
social:
|
||||
- icon: fontawesome/brands/github-alt
|
||||
link: https://github.com/tiangolo/sqlmodel
|
||||
|
@ -17,10 +17,10 @@ classifiers = [
|
||||
"Intended Audience :: System Administrators",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Topic :: Database",
|
||||
"Topic :: Database :: Database Engines/Servers",
|
||||
"Topic :: Internet",
|
||||
@ -30,28 +30,24 @@ classifiers = [
|
||||
]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.6.1"
|
||||
SQLAlchemy = ">=1.4.17,<=1.4.41"
|
||||
pydantic = "^1.8.2"
|
||||
python = "^3.7"
|
||||
SQLAlchemy = ">=1.4.36,<2.0.0"
|
||||
pydantic = "^1.9.0"
|
||||
sqlalchemy2-stubs = {version = "*", allow-prereleases = true}
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = "^7.0.1"
|
||||
mypy = "0.971"
|
||||
flake8 = "^5.0.4"
|
||||
black = {version = "^22.10.0", python = "^3.7"}
|
||||
mkdocs = "^1.2.1"
|
||||
mkdocs-material = "^8.1.4"
|
||||
pillow = {version = "^9.3.0", python = "^3.7"}
|
||||
cairosvg = {version = "^2.5.2", python = "^3.7"}
|
||||
# Needed by the code generator using templates
|
||||
black = "^22.10.0"
|
||||
mkdocs-material = "9.1.21"
|
||||
pillow = "^9.3.0"
|
||||
cairosvg = "^2.5.2"
|
||||
mdx-include = "^1.4.1"
|
||||
coverage = {extras = ["toml"], version = "^6.2"}
|
||||
fastapi = "^0.68.1"
|
||||
requests = "^2.26.0"
|
||||
autoflake = "^1.4"
|
||||
isort = "^5.9.3"
|
||||
async_generator = {version = "*", python = "~3.6"}
|
||||
async-exit-stack = {version = "*", python = "~3.6"}
|
||||
ruff = "^0.1.2"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
@ -77,33 +73,36 @@ exclude_lines = [
|
||||
"if TYPE_CHECKING:",
|
||||
]
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
known_third_party = ["sqlmodel"]
|
||||
skip_glob = [
|
||||
"sqlmodel/__init__.py",
|
||||
]
|
||||
|
||||
|
||||
[tool.mypy]
|
||||
# --strict
|
||||
disallow_any_generics = true
|
||||
disallow_subclassing_any = true
|
||||
disallow_untyped_calls = true
|
||||
disallow_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
check_untyped_defs = true
|
||||
disallow_untyped_decorators = true
|
||||
no_implicit_optional = true
|
||||
warn_redundant_casts = true
|
||||
warn_unused_ignores = true
|
||||
warn_return_any = true
|
||||
implicit_reexport = false
|
||||
strict_equality = true
|
||||
# --strict end
|
||||
strict = true
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = "sqlmodel.sql.expression"
|
||||
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
|
||||
|
@ -2,4 +2,6 @@
|
||||
|
||||
set -e
|
||||
|
||||
export DYLD_FALLBACK_LIBRARY_PATH="/opt/homebrew/lib"
|
||||
|
||||
mkdocs serve --dev-addr 127.0.0.1:8008
|
||||
|
@ -1,6 +1,5 @@
|
||||
#!/bin/sh -e
|
||||
set -x
|
||||
|
||||
autoflake --remove-all-unused-imports --recursive --remove-unused-variables --in-place sqlmodel docs_src tests --exclude=__init__.py
|
||||
black sqlmodel tests docs_src
|
||||
isort sqlmodel tests docs_src
|
||||
ruff sqlmodel tests docs_src scripts --fix
|
||||
ruff format sqlmodel tests docs_src scripts
|
||||
|
@ -4,8 +4,5 @@ set -e
|
||||
set -x
|
||||
|
||||
mypy sqlmodel
|
||||
flake8 sqlmodel tests docs_src
|
||||
black 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
|
||||
ruff sqlmodel tests docs_src scripts
|
||||
ruff format sqlmodel tests docs_src --check
|
||||
|
@ -3,6 +3,7 @@
|
||||
set -e
|
||||
set -x
|
||||
|
||||
CHECK_JINJA=1 python scripts/generate_select.py
|
||||
coverage run -m pytest tests
|
||||
coverage combine
|
||||
coverage report --show-missing
|
||||
|
@ -1,16 +1,16 @@
|
||||
__version__ = "0.0.8"
|
||||
__version__ = "0.0.10"
|
||||
|
||||
# Re-export from SQLAlchemy
|
||||
from sqlalchemy.engine import create_mock_engine as create_mock_engine
|
||||
from sqlalchemy.engine import engine_from_config as engine_from_config
|
||||
from sqlalchemy.inspection import inspect as inspect
|
||||
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 Column as Column
|
||||
from sqlalchemy.schema import ColumnDefault as ColumnDefault
|
||||
from sqlalchemy.schema import Computed as Computed
|
||||
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 FetchedValue as FetchedValue
|
||||
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 ThreadLocalMetaData as ThreadLocalMetaData
|
||||
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 all_ as all_
|
||||
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_all as intersect_all
|
||||
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 lateral as lateral
|
||||
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.types import ARRAY as ARRAY
|
||||
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 BLOB as BLOB
|
||||
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 CLOB as CLOB
|
||||
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 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 INT as INT
|
||||
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 LargeBinary as LargeBinary
|
||||
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 NVARCHAR as NVARCHAR
|
||||
from sqlalchemy.types import PickleType as PickleType
|
||||
from sqlalchemy.types import REAL as REAL
|
||||
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 String as String
|
||||
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 TIMESTAMP as TIMESTAMP
|
||||
from sqlalchemy.types import TypeDecorator as TypeDecorator
|
||||
from sqlalchemy.types import Unicode as Unicode
|
||||
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 .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 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
|
||||
|
@ -6,7 +6,7 @@ class _DefaultPlaceholder:
|
||||
You shouldn't use this class directly.
|
||||
|
||||
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):
|
||||
@ -27,6 +27,6 @@ def Default(value: _TDefaultType) -> _TDefaultType:
|
||||
You shouldn't use this function directly.
|
||||
|
||||
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
|
||||
|
@ -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.ext.asyncio import AsyncSession as _AsyncSession
|
||||
from sqlalchemy.ext.asyncio import engine
|
||||
from sqlalchemy.ext.asyncio.engine import AsyncConnection, AsyncEngine
|
||||
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 ...sql.expression import Select
|
||||
from ...sql.base import Executable
|
||||
from ...sql.expression import Select, SelectOfScalar
|
||||
|
||||
_T = TypeVar("_T")
|
||||
_TSelectParam = TypeVar("_TSelectParam")
|
||||
|
||||
|
||||
class AsyncSession(_AsyncSession):
|
||||
@ -40,14 +40,46 @@ class AsyncSession(_AsyncSession):
|
||||
Session(bind=bind, binds=binds, **kw) # type: ignore
|
||||
)
|
||||
|
||||
@overload
|
||||
async def exec(
|
||||
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,
|
||||
execution_options: Mapping[Any, Any] = util.EMPTY_DICT,
|
||||
bind_arguments: Optional[Mapping[str, Any]] = None,
|
||||
**kw: Any,
|
||||
) -> ScalarResult[_T]:
|
||||
) -> Union[Result[_TSelectParam], ScalarResult[_TSelectParam]]:
|
||||
# TODO: the documentation says execution_options accepts a dict, but only
|
||||
# util.immutabledict has the union() method. Is this a bug in SQLAlchemy?
|
||||
execution_options = execution_options.union({"prebuffer_rows": True}) # type: ignore
|
||||
|
210
sqlmodel/main.py
210
sqlmodel/main.py
@ -11,6 +11,7 @@ from typing import (
|
||||
Callable,
|
||||
ClassVar,
|
||||
Dict,
|
||||
ForwardRef,
|
||||
List,
|
||||
Mapping,
|
||||
Optional,
|
||||
@ -21,19 +22,29 @@ from typing import (
|
||||
TypeVar,
|
||||
Union,
|
||||
cast,
|
||||
overload,
|
||||
)
|
||||
|
||||
from pydantic import BaseConfig, BaseModel
|
||||
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 ModelField, Undefined, UndefinedType
|
||||
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 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 Float, ForeignKey, Integer, Interval, Numeric, inspect
|
||||
from sqlalchemy.orm import RelationshipProperty, declared_attr, registry, relationship
|
||||
from sqlalchemy.orm.attributes import set_attribute
|
||||
from sqlalchemy.orm.decl_api import DeclarativeMeta
|
||||
@ -78,6 +89,28 @@ class FieldInfo(PydanticFieldInfo):
|
||||
"Passing sa_column_kwargs is not supported when "
|
||||
"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)
|
||||
self.primary_key = primary_key
|
||||
self.nullable = nullable
|
||||
@ -118,6 +151,7 @@ class RelationshipInfo(Representation):
|
||||
self.sa_relationship_kwargs = sa_relationship_kwargs
|
||||
|
||||
|
||||
@overload
|
||||
def Field(
|
||||
default: Any = Undefined,
|
||||
*,
|
||||
@ -137,15 +171,99 @@ def Field(
|
||||
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,
|
||||
primary_key: bool = False,
|
||||
foreign_key: Optional[Any] = None,
|
||||
unique: bool = False,
|
||||
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,
|
||||
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,
|
||||
index: Union[bool, UndefinedType] = Undefined,
|
||||
sa_type: Type[Any] = Undefined,
|
||||
@ -169,12 +287,17 @@ def Field(
|
||||
lt=lt,
|
||||
le=le,
|
||||
multiple_of=multiple_of,
|
||||
max_digits=max_digits,
|
||||
decimal_places=decimal_places,
|
||||
min_items=min_items,
|
||||
max_items=max_items,
|
||||
unique_items=unique_items,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
allow_mutation=allow_mutation,
|
||||
regex=regex,
|
||||
discriminator=discriminator,
|
||||
repr=repr,
|
||||
primary_key=primary_key,
|
||||
foreign_key=foreign_key,
|
||||
unique=unique,
|
||||
@ -190,6 +313,27 @@ def Field(
|
||||
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(
|
||||
*,
|
||||
back_populates: Optional[str] = None,
|
||||
@ -308,9 +452,9 @@ class SQLModelMetaclass(ModelMetaclass, DeclarativeMeta):
|
||||
config_registry = cast(registry, config_registry)
|
||||
# If it was passed by kwargs, ensure it's also set in config
|
||||
new_cls.__config__.registry = config_table
|
||||
setattr(new_cls, "_sa_registry", config_registry)
|
||||
setattr(new_cls, "metadata", config_registry.metadata)
|
||||
setattr(new_cls, "__abstract__", True)
|
||||
setattr(new_cls, "_sa_registry", config_registry) # noqa: B010
|
||||
setattr(new_cls, "metadata", config_registry.metadata) # noqa: B010
|
||||
setattr(new_cls, "__abstract__", True) # noqa: B010
|
||||
return new_cls
|
||||
|
||||
# Override SQLAlchemy, allow both SQLAlchemy and plain Pydantic models
|
||||
@ -323,19 +467,16 @@ class SQLModelMetaclass(ModelMetaclass, DeclarativeMeta):
|
||||
# triggers an error
|
||||
base_is_table = False
|
||||
for base in bases:
|
||||
config = getattr(base, "__config__")
|
||||
config = getattr(base, "__config__") # noqa: B009
|
||||
if config and getattr(config, "table", False):
|
||||
base_is_table = True
|
||||
break
|
||||
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():
|
||||
if rel_info.sa_relationship:
|
||||
# There's a SQLAlchemy relationship declared, that takes precedence
|
||||
# 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
|
||||
ann = cls.__annotations__[rel_name]
|
||||
temp_field = ModelField.infer(
|
||||
@ -353,7 +494,7 @@ class SQLModelMetaclass(ModelMetaclass, DeclarativeMeta):
|
||||
rel_kwargs["back_populates"] = rel_info.back_populates
|
||||
if 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:
|
||||
raise RuntimeError(
|
||||
"Couldn't find the secondary table for "
|
||||
@ -368,9 +509,11 @@ class SQLModelMetaclass(ModelMetaclass, DeclarativeMeta):
|
||||
rel_value: RelationshipProperty = relationship( # type: ignore
|
||||
relationship_to, *rel_args, **rel_kwargs
|
||||
)
|
||||
dict_used[rel_name] = rel_value
|
||||
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:
|
||||
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 not issubclass(type(field.field_info.sa_type), type(Undefined)):
|
||||
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 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
|
||||
if issubclass(field.type_, time):
|
||||
return Time
|
||||
if issubclass(field.type_, Enum):
|
||||
return sa_Enum(field.type_)
|
||||
if issubclass(field.type_, bytes):
|
||||
return LargeBinary
|
||||
if issubclass(field.type_, Decimal):
|
||||
@ -426,21 +571,28 @@ def get_column_from_field(field: ModelField) -> Column: # type: ignore
|
||||
if isinstance(sa_column, Column):
|
||||
return sa_column
|
||||
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)
|
||||
if index is Undefined:
|
||||
index = False
|
||||
nullable = not primary_key and _is_field_noneable(field)
|
||||
# Override derived nullability if the nullable property is set explicitly
|
||||
# on the field
|
||||
if hasattr(field.field_info, "nullable"):
|
||||
field_nullable = getattr(field.field_info, "nullable")
|
||||
field_nullable = getattr(field.field_info, "nullable", Undefined) # noqa: B009
|
||||
if field_nullable != Undefined:
|
||||
assert not isinstance(field_nullable, UndefinedType)
|
||||
nullable = field_nullable
|
||||
args = []
|
||||
foreign_key = getattr(field.field_info, "foreign_key", None)
|
||||
unique = getattr(field.field_info, "unique", False)
|
||||
foreign_key = getattr(field.field_info, "foreign_key", Undefined)
|
||||
if foreign_key is Undefined:
|
||||
foreign_key = None
|
||||
unique = getattr(field.field_info, "unique", Undefined)
|
||||
if unique is Undefined:
|
||||
unique = False
|
||||
if foreign_key:
|
||||
assert isinstance(foreign_key, str)
|
||||
args.append(ForeignKey(foreign_key))
|
||||
kwargs = {
|
||||
"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]]:
|
||||
# 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
|
||||
@classmethod
|
||||
|
@ -4,11 +4,11 @@ from sqlalchemy import util
|
||||
from sqlalchemy.orm import Query as _Query
|
||||
from sqlalchemy.orm import Session as _Session
|
||||
from sqlalchemy.sql.base import Executable as _Executable
|
||||
from sqlmodel.sql.expression import Select, SelectOfScalar
|
||||
from typing_extensions import Literal
|
||||
|
||||
from ..engine.result import Result, ScalarResult
|
||||
from ..sql.base import Executable
|
||||
from ..sql.expression import Select, SelectOfScalar
|
||||
|
||||
_TSelectParam = TypeVar("_TSelectParam")
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
# WARNING: do not modify this code, it is generated by expression.py.jinja2
|
||||
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
@ -12,7 +11,6 @@ from typing import (
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
cast,
|
||||
overload,
|
||||
)
|
||||
from uuid import UUID
|
||||
@ -24,37 +22,18 @@ from sqlalchemy.sql.expression import Select as _Select
|
||||
|
||||
_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
|
||||
|
||||
# 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
|
||||
# entity, so the result will be converted to a scalar by default. This way writing
|
||||
# for loops on the results will feel natural.
|
||||
class SelectOfScalar(_Select, Generic[_TSelect]):
|
||||
|
||||
# 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
|
||||
# entity, so the result will be converted to a scalar by default. This way writing
|
||||
# for loops on the results will feel natural.
|
||||
class SelectOfScalar(_Select, Generic[_TSelect]):
|
||||
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
|
||||
from ..main import SQLModel
|
||||
|
@ -1,4 +1,3 @@
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
@ -10,7 +9,6 @@ from typing import (
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
cast,
|
||||
overload,
|
||||
)
|
||||
from uuid import UUID
|
||||
@ -22,38 +20,16 @@ from sqlalchemy.sql.expression import Select as _Select
|
||||
|
||||
_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
|
||||
|
||||
# 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
|
||||
# entity, so the result will be converted to a scalar by default. This way writing
|
||||
# for loops on the results will feel natural.
|
||||
class SelectOfScalar(_Select, Generic[_TSelect]):
|
||||
# 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
|
||||
# entity, so the result will be converted to a scalar by default. This way writing
|
||||
# for loops on the results will feel natural.
|
||||
class SelectOfScalar(_Select, Generic[_TSelect]):
|
||||
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
|
||||
from ..main import SQLModel
|
||||
|
||||
|
@ -8,7 +8,6 @@ from sqlalchemy.sql.type_api import TypeEngine
|
||||
|
||||
|
||||
class AutoString(types.TypeDecorator): # type: ignore
|
||||
|
||||
impl = types.String
|
||||
cache_ok = True
|
||||
mysql_default_length = 255
|
||||
|
@ -42,8 +42,7 @@ def coverage_run(*, module: str, cwd: Union[str, Path]) -> subprocess.CompletedP
|
||||
module,
|
||||
],
|
||||
cwd=str(cwd),
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
capture_output=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
return result
|
||||
|
@ -14,12 +14,12 @@ Associated issues:
|
||||
"""
|
||||
|
||||
|
||||
class MyEnum1(enum.Enum):
|
||||
class MyEnum1(str, enum.Enum):
|
||||
A = "A"
|
||||
B = "B"
|
||||
|
||||
|
||||
class MyEnum2(enum.Enum):
|
||||
class MyEnum2(str, enum.Enum):
|
||||
C = "C"
|
||||
D = "D"
|
||||
|
||||
@ -70,3 +70,43 @@ def test_sqlite_ddl_sql(capsys):
|
||||
captured = capsys.readouterr()
|
||||
assert "enum_field VARCHAR(1) NOT NULL" 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",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
39
tests/test_field_sa_args_kwargs.py
Normal file
39
tests/test_field_sa_args_kwargs.py
Normal 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
|
99
tests/test_field_sa_column.py
Normal file
99
tests/test_field_sa_column.py
Normal 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),
|
||||
)
|
53
tests/test_field_sa_relationship.py
Normal file
53
tests/test_field_sa_relationship.py
Normal 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")
|
@ -1,8 +1,9 @@
|
||||
from typing import Optional
|
||||
from typing import List, Optional
|
||||
|
||||
import pytest
|
||||
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):
|
||||
@ -91,3 +92,37 @@ def test_should_raise_exception_when_try_to_duplicate_row_if_unique_constraint_i
|
||||
session.add(hero_2)
|
||||
session.commit()
|
||||
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"
|
||||
|
0
tests/test_pydantic/__init__.py
Normal file
0
tests/test_pydantic/__init__.py
Normal file
57
tests/test_pydantic/test_field.py
Normal file
57
tests/test_pydantic/test_field.py
Normal 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)
|
28
tests/test_sqlalchemy_type_errors.py
Normal file
28
tests/test_sqlalchemy_type_errors.py
Normal 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]
|
@ -2,7 +2,63 @@ from fastapi.testclient import TestClient
|
||||
from sqlmodel import create_engine
|
||||
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",
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"},
|
||||
"paths": {
|
||||
@ -13,7 +69,11 @@ openapi_schema = {
|
||||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "Offset", "type": "integer", "default": 0},
|
||||
"schema": {
|
||||
"title": "Offset",
|
||||
"type": "integer",
|
||||
"default": 0,
|
||||
},
|
||||
"name": "offset",
|
||||
"in": "query",
|
||||
},
|
||||
@ -21,9 +81,9 @@ openapi_schema = {
|
||||
"required": False,
|
||||
"schema": {
|
||||
"title": "Limit",
|
||||
"maximum": 100.0,
|
||||
"type": "integer",
|
||||
"default": 100,
|
||||
"lte": 100,
|
||||
},
|
||||
"name": "limit",
|
||||
"in": "query",
|
||||
@ -37,7 +97,9 @@ openapi_schema = {
|
||||
"schema": {
|
||||
"title": "Response Read Heroes Heroes Get",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/HeroRead"},
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/HeroRead"
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -60,7 +122,9 @@ openapi_schema = {
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/HeroCreate"}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HeroCreate"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
@ -70,7 +134,9 @@ openapi_schema = {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/HeroRead"}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HeroRead"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
@ -104,7 +170,9 @@ openapi_schema = {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/HeroRead"}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HeroRead"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
@ -162,7 +230,9 @@ openapi_schema = {
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/HeroUpdate"}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HeroUpdate"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
@ -172,7 +242,9 @@ openapi_schema = {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/HeroRead"}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HeroRead"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
@ -199,7 +271,9 @@ openapi_schema = {
|
||||
"detail": {
|
||||
"title": "Detail",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ValidationError"
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
@ -249,63 +323,4 @@ 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
|
||||
|
@ -2,7 +2,68 @@ from fastapi.testclient import TestClient
|
||||
from sqlmodel import create_engine
|
||||
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",
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"},
|
||||
"paths": {
|
||||
@ -13,7 +74,11 @@ openapi_schema = {
|
||||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "Offset", "type": "integer", "default": 0},
|
||||
"schema": {
|
||||
"title": "Offset",
|
||||
"type": "integer",
|
||||
"default": 0,
|
||||
},
|
||||
"name": "offset",
|
||||
"in": "query",
|
||||
},
|
||||
@ -21,9 +86,9 @@ openapi_schema = {
|
||||
"required": False,
|
||||
"schema": {
|
||||
"title": "Limit",
|
||||
"maximum": 100.0,
|
||||
"type": "integer",
|
||||
"default": 100,
|
||||
"lte": 100,
|
||||
},
|
||||
"name": "limit",
|
||||
"in": "query",
|
||||
@ -37,7 +102,9 @@ openapi_schema = {
|
||||
"schema": {
|
||||
"title": "Response Read Heroes Heroes Get",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/HeroRead"},
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/HeroRead"
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -60,7 +127,9 @@ openapi_schema = {
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/HeroCreate"}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HeroCreate"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
@ -70,7 +139,9 @@ openapi_schema = {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/HeroRead"}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HeroRead"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
@ -104,7 +175,9 @@ openapi_schema = {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/HeroRead"}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HeroRead"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
@ -131,7 +204,9 @@ openapi_schema = {
|
||||
"detail": {
|
||||
"title": "Detail",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ValidationError"
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
@ -172,68 +247,4 @@ 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"]
|
||||
|
@ -123,7 +123,6 @@ def test_tutorial(clear_sqlmodel):
|
||||
)
|
||||
|
||||
with TestClient(mod.app) as client:
|
||||
|
||||
hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"}
|
||||
hero2_data = {
|
||||
"name": "Spider-Boy",
|
||||
@ -173,8 +172,18 @@ def test_tutorial(clear_sqlmodel):
|
||||
insp: Inspector = inspect(mod.engine)
|
||||
indexes = insp.get_indexes(str(mod.Hero.__tablename__))
|
||||
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:
|
||||
assert index in indexes, "This expected index should be in the indexes in DB"
|
||||
|
@ -123,7 +123,6 @@ def test_tutorial(clear_sqlmodel):
|
||||
)
|
||||
|
||||
with TestClient(mod.app) as client:
|
||||
|
||||
hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"}
|
||||
hero2_data = {
|
||||
"name": "Spider-Boy",
|
||||
@ -173,8 +172,18 @@ def test_tutorial(clear_sqlmodel):
|
||||
insp: Inspector = inspect(mod.engine)
|
||||
indexes = insp.get_indexes(str(mod.Hero.__tablename__))
|
||||
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:
|
||||
assert index in indexes, "This expected index should be in the indexes in DB"
|
||||
|
@ -155,7 +155,6 @@ def test_tutorial(clear_sqlmodel):
|
||||
)
|
||||
|
||||
with TestClient(mod.app) as client:
|
||||
|
||||
hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"}
|
||||
hero2_data = {
|
||||
"name": "Spider-Boy",
|
||||
|
@ -2,7 +2,111 @@ from fastapi.testclient import TestClient
|
||||
from sqlmodel import create_engine
|
||||
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 Margaret’s 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",
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"},
|
||||
"paths": {
|
||||
@ -13,7 +117,11 @@ openapi_schema = {
|
||||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "Offset", "type": "integer", "default": 0},
|
||||
"schema": {
|
||||
"title": "Offset",
|
||||
"type": "integer",
|
||||
"default": 0,
|
||||
},
|
||||
"name": "offset",
|
||||
"in": "query",
|
||||
},
|
||||
@ -21,9 +129,9 @@ openapi_schema = {
|
||||
"required": False,
|
||||
"schema": {
|
||||
"title": "Limit",
|
||||
"maximum": 100.0,
|
||||
"type": "integer",
|
||||
"default": 100,
|
||||
"lte": 100,
|
||||
},
|
||||
"name": "limit",
|
||||
"in": "query",
|
||||
@ -37,7 +145,9 @@ openapi_schema = {
|
||||
"schema": {
|
||||
"title": "Response Read Heroes Heroes Get",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/HeroRead"},
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/HeroRead"
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -60,7 +170,9 @@ openapi_schema = {
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/HeroCreate"}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HeroCreate"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
@ -70,7 +182,9 @@ openapi_schema = {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/HeroRead"}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HeroRead"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
@ -164,7 +278,9 @@ openapi_schema = {
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/HeroUpdate"}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HeroUpdate"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
@ -174,7 +290,9 @@ openapi_schema = {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/HeroRead"}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HeroRead"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
@ -198,7 +316,11 @@ openapi_schema = {
|
||||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "Offset", "type": "integer", "default": 0},
|
||||
"schema": {
|
||||
"title": "Offset",
|
||||
"type": "integer",
|
||||
"default": 0,
|
||||
},
|
||||
"name": "offset",
|
||||
"in": "query",
|
||||
},
|
||||
@ -206,9 +328,9 @@ openapi_schema = {
|
||||
"required": False,
|
||||
"schema": {
|
||||
"title": "Limit",
|
||||
"maximum": 100.0,
|
||||
"type": "integer",
|
||||
"default": 100,
|
||||
"lte": 100,
|
||||
},
|
||||
"name": "limit",
|
||||
"in": "query",
|
||||
@ -222,7 +344,9 @@ openapi_schema = {
|
||||
"schema": {
|
||||
"title": "Response Read Teams Teams Get",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/TeamRead"},
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/TeamRead"
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -245,7 +369,9 @@ openapi_schema = {
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/TeamCreate"}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TeamCreate"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
@ -255,7 +381,9 @@ openapi_schema = {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/TeamRead"}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TeamRead"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
@ -349,7 +477,9 @@ openapi_schema = {
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/TeamUpdate"}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TeamUpdate"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
@ -359,7 +489,9 @@ openapi_schema = {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/TeamRead"}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TeamRead"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
@ -386,7 +518,9 @@ openapi_schema = {
|
||||
"detail": {
|
||||
"title": "Detail",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ValidationError"
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
@ -496,111 +630,4 @@ 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 Margaret’s 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
|
||||
|
@ -111,7 +111,6 @@ def test_tutorial(clear_sqlmodel):
|
||||
)
|
||||
|
||||
with TestClient(mod.app) as client:
|
||||
|
||||
hero_data = {"name": "Deadpond", "secret_name": "Dive Wilson"}
|
||||
response = client.post("/heroes/", json=hero_data)
|
||||
data = response.json()
|
||||
|
@ -2,7 +2,63 @@ from fastapi.testclient import TestClient
|
||||
from sqlmodel import create_engine
|
||||
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",
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"},
|
||||
"paths": {
|
||||
@ -13,7 +69,11 @@ openapi_schema = {
|
||||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "Offset", "type": "integer", "default": 0},
|
||||
"schema": {
|
||||
"title": "Offset",
|
||||
"type": "integer",
|
||||
"default": 0,
|
||||
},
|
||||
"name": "offset",
|
||||
"in": "query",
|
||||
},
|
||||
@ -21,9 +81,9 @@ openapi_schema = {
|
||||
"required": False,
|
||||
"schema": {
|
||||
"title": "Limit",
|
||||
"maximum": 100.0,
|
||||
"type": "integer",
|
||||
"default": 100,
|
||||
"lte": 100,
|
||||
},
|
||||
"name": "limit",
|
||||
"in": "query",
|
||||
@ -37,7 +97,9 @@ openapi_schema = {
|
||||
"schema": {
|
||||
"title": "Response Read Heroes Heroes Get",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/HeroRead"},
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/HeroRead"
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -60,7 +122,9 @@ openapi_schema = {
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/HeroCreate"}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HeroCreate"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
@ -70,7 +134,9 @@ openapi_schema = {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/HeroRead"}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HeroRead"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
@ -104,7 +170,9 @@ openapi_schema = {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/HeroRead"}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HeroRead"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
@ -162,7 +230,9 @@ openapi_schema = {
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/HeroUpdate"}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HeroUpdate"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
@ -172,7 +242,9 @@ openapi_schema = {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/HeroRead"}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HeroRead"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
@ -199,7 +271,9 @@ openapi_schema = {
|
||||
"detail": {
|
||||
"title": "Detail",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ValidationError"
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
@ -249,63 +323,4 @@ 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
|
||||
|
@ -99,7 +99,6 @@ def test_tutorial(clear_sqlmodel):
|
||||
)
|
||||
|
||||
with TestClient(mod.app) as client:
|
||||
|
||||
hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"}
|
||||
hero2_data = {
|
||||
"name": "Spider-Boy",
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user