├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.yml
│ └── config.yml
├── scripts
│ └── releases.py
└── workflows
│ ├── lint.yml
│ ├── pypi-publish.yml
│ ├── pytest.yml
│ └── update-release-docs.yml
├── .gitignore
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── docs
└── en
│ ├── docs
│ ├── assets
│ │ ├── RouteDetail.png
│ │ ├── RouteOverview.png
│ │ ├── banner.png
│ │ ├── bolt-grad.svg
│ │ ├── bolt.svg
│ │ └── logo.png
│ ├── backends
│ │ ├── async.md
│ │ ├── gino.md
│ │ ├── memory.md
│ │ ├── ormar.md
│ │ ├── sqlalchemy.md
│ │ └── tortoise.md
│ ├── contributing.md
│ ├── css
│ │ ├── main.css
│ │ └── termynal.css
│ ├── dependencies.md
│ ├── index.md
│ ├── js
│ │ ├── termy.js
│ │ └── termynal.js
│ ├── pagination.md
│ ├── releases.md
│ ├── routing.md
│ └── schemas.md
│ ├── mkdocs.yml
│ ├── overrides
│ └── main.html
│ └── vercel.json
├── fastapi_crudrouter
├── __init__.py
├── _version.py
├── core
│ ├── __init__.py
│ ├── _base.py
│ ├── _types.py
│ ├── _utils.py
│ ├── databases.py
│ ├── gino_starlette.py
│ ├── mem.py
│ ├── ormar.py
│ ├── sqlalchemy.py
│ └── tortoise.py
└── py.typed
├── requirements.txt
├── setup.cfg
├── setup.py
└── tests
├── __init__.py
├── conf
├── __init__.py
├── config.py
├── dev.docker-compose.yml
└── dev.env
├── conftest.py
├── dev.requirements.txt
├── implementations
├── __init__.py
├── databases_.py
├── gino_.py
├── memory.py
├── ormar_.py
├── sqlalchemy_.py
└── tortoise_.py
├── test_base.py
├── test_custom_ids.py
├── test_dependencies
├── test_disable.py
├── test_per_route.py
└── test_top_level.py
├── test_exclude.py
├── test_integration
├── __init__.py
├── test_backend_not_installed.py
└── test_typing.py
├── test_integrity_errors.py
├── test_openapi_schema.py
├── test_overloads.py
├── test_pagination.py
├── test_pks.py
├── test_prefix.py
├── test_router.py
├── test_sqlalchemy_nested_.py
├── test_two_routers.py
├── test_version.py
└── utils.py
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: awtkns
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | name: 🐞 Bug / Issue Report
2 | description: File a bug/issue
3 | title: "🐛
"
4 | labels: [Bug, Needs Triage]
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: Thanks for your intrest in FastAPI-Crudrouter!
9 | - type: textarea
10 | id: description
11 | attributes:
12 | label: Description
13 | description: |
14 | What is the problem, question, or error?
15 | Write a short description detailing what you are doing, what you expect to happen, and what is currently happening.
16 | placeholder: |
17 | * Open the browser and call the endpoint `/`.
18 | * It returns a JSON with `{"Hello": "World"}`.
19 | * But I expected it to return `{"Hello": "Sara"}`.
20 | validations:
21 | required: true
22 | - type: textarea
23 | attributes:
24 | label: Example Code / Steps To Reproduce
25 | description: |
26 | Please add a self-contained, minimal, and reproducible example and or stack trace of the issue you are currently facing. Doing so
27 | increases the chances of the issue your a facing being quickly fixed.
28 | placeholder: |
29 | ```from pydantic import BaseModel
30 | from fastapi import FastAPI
31 | from fastapi_crudrouter import MemoryCRUDRouter as CRUDRouter
32 |
33 | class Potato(BaseModel):
34 | id: int
35 | color: str
36 | mass: float
37 |
38 | app = FastAPI()
39 | app.include_router(CRUDRouter(schema=Potato))```
40 | validations:
41 | required: false
42 | - type: checkboxes
43 | id: operating-systems
44 | attributes:
45 | label: On which operating system are you facing this issue?
46 | description: You may select more than one.
47 | options:
48 | - label: Windows
49 | - label: Mac
50 | - label: Linux
51 | validations:
52 | required: true
53 | - type: input
54 | id: fastapi_crouter-version
55 | attributes:
56 | label: FastAPI-Crudrouter Version
57 | description: |
58 | What version of FastApi-Crudrouter are you using?
59 |
60 | You can find the FastApi-Crudrouter version with:
61 | ```bash
62 | python -c "import fastapi_crudrouter; print(fastapi_crudrouter.__version__)"
63 | ```
64 | validations:
65 | required: true
66 | - type: input
67 | id: python-version
68 | attributes:
69 | label: Python Version
70 | description: |
71 | What Python version are you using?
72 |
73 | You can find the Python version with:
74 | ```bash
75 | python --version
76 | ```
77 | validations:
78 | required: true
79 | - type: dropdown
80 | id: contributing
81 | attributes:
82 | label: |
83 | Would you be intrested in PR'ing / contributing to the fix for this issue. New contributors to fastapi-crudrouter are
84 | always welcome. See the [contributing docs](https://fastapi-crudrouter.awtkns.com/contributing) for more detials.
85 | options:
86 | - Yes
87 | - No
88 | - Unsure
89 | validations:
90 | required: false
91 | - type: textarea
92 | attributes:
93 | label: Anything else?
94 | description: |
95 | Links? References? Anything that will give us more context about the issue you are encountering!
96 |
97 | Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
98 | validations:
99 | required: false
100 | - type: markdown
101 | attributes:
102 | value: Thanks for taking the time to submit this issue. By doing so, you're improving the package for everyone. 🎉
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: true
2 |
--------------------------------------------------------------------------------
/.github/scripts/releases.py:
--------------------------------------------------------------------------------
1 | import re
2 | from os import environ
3 |
4 | from github import Github
5 | from github.GitRelease import GitRelease
6 |
7 | GITHUB_REPOSITORY = environ.get("GITHUB_REPOSITORY", "awtkns/fastapi-crudrouter")
8 | GITHUB_TOKEN = environ.get("GH_TOKEN") or environ.get("GITHUB_TOKEN")
9 | GITHUB_URL = "https://github.com"
10 | GITHUB_BRANCH = "master"
11 | FILE_PATH = "docs/en/docs/releases.md"
12 | COMMIT_MESSAGE = "🤖 auto update releases.md"
13 |
14 |
15 | gh = Github(GITHUB_TOKEN)
16 |
17 |
18 | def generate_header(r: GitRelease, header_row: bool = False):
19 | header = "Release Notes\n===\n" if header_row else "\n\n---\n"
20 |
21 | return f"""{header}
22 | ## [{r.title}]({r.html_url}){" { .releases } "}
23 | {r.created_at.date()}
24 | """
25 |
26 |
27 | def commit_update(content: str):
28 | file = repo.get_contents(FILE_PATH)
29 | old_content = file.decoded_content.decode()
30 |
31 | if new_content == old_content:
32 | print("No new release information, Skipping.")
33 | else:
34 | print("Uploading new release documentation")
35 |
36 | repo.update_file(
37 | file.path,
38 | message=COMMIT_MESSAGE,
39 | content=content,
40 | sha=file.sha,
41 | branch=GITHUB_BRANCH,
42 | )
43 |
44 |
45 | def insert_links(content: str):
46 | """Replaces both #pull and @author with correct links"""
47 | pull_url = repo.html_url + "/pull"
48 | content = re.sub(r"#(\d+)", rf"[#\1]({pull_url}/\1)", content)
49 |
50 | compare_url = repo.html_url + "/compare"
51 | content = re.sub(rf"{compare_url}/([^\s]+)", rf"[`\1`]({compare_url}/\1)", content)
52 |
53 | return re.sub(r"@(\S+)", rf"[@\1]({GITHUB_URL}/\1)", content)
54 |
55 |
56 | if __name__ == "__main__":
57 | repo = gh.get_repo(GITHUB_REPOSITORY)
58 |
59 | new_content = ""
60 | show_header = True
61 | for release in repo.get_releases():
62 | if not release.draft:
63 | new_content += generate_header(release, header_row=show_header)
64 | new_content += release.body
65 | show_header = False
66 |
67 | new_content = insert_links(new_content)
68 | commit_update(new_content)
69 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: Lint
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | lint:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v2
14 | - name: Set up Python
15 | uses: actions/setup-python@v2
16 | - name: Run Black Code Formatter
17 | uses: psf/black@stable
18 | - name: Install dependencies
19 | run: |
20 | python -m pip install --upgrade pip
21 | pip install -r tests/dev.requirements.txt
22 | - name: Check Typing with mypy
23 | run: |
24 | mypy fastapi_crudrouter
25 | - name: Lint with flake8
26 | run: |
27 | flake8 fastapi_crudrouter
28 |
--------------------------------------------------------------------------------
/.github/workflows/pypi-publish.yml:
--------------------------------------------------------------------------------
1 | # This workflows will upload a Python Package using Twine when a release is created
2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
3 |
4 | name: Upload Python Package
5 |
6 | on:
7 | push:
8 | branches:
9 | - master
10 |
11 | jobs:
12 | deploy:
13 |
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - uses: actions/checkout@v2
18 | - name: Set up Python
19 | uses: actions/setup-python@v2
20 | with:
21 | python-version: '3.10'
22 | - name: Bump Version
23 | env:
24 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
25 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
26 | if: github.event.pusher.name == 'awtkns'
27 | run: |
28 | curl -s https://pypi.org/pypi/fastapi-crudrouter/json | grep -Eo '"version":"[0-9].[0-9][0-9]?.[0-9][0-9]?"' | grep -Eo "[0-9].[0-9][0-9]?.[0-9][0-9]?" > old
29 | grep -Eo '__version__ = "[0-9].[0-9][0-9]?.[0-9][0-9]?"' ./fastapi_crudrouter/_version.py | grep -Eo "[0-9].[0-9][0-9]?.[0-9][0-9]?" > new
30 |
31 | cat new
32 | cat old
33 | if cmp --silent new old; then
34 | echo --- SKIPPING VERSION BUMP ---
35 | else
36 | echo ---BUMPING VERSION---
37 |
38 | python -m pip install --upgrade pip
39 | pip install setuptools wheel twine
40 |
41 | python setup.py sdist bdist_wheel
42 | twine upload dist/*
43 | fi
44 |
--------------------------------------------------------------------------------
/.github/workflows/pytest.yml:
--------------------------------------------------------------------------------
1 | name: Python application
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | test:
11 | runs-on: ubuntu-latest
12 | strategy:
13 | matrix:
14 | python-version: [ "3.7", "3.8", "3.9", "3.10", "3.11" ]
15 | services:
16 | postgres:
17 | image: postgres
18 | env:
19 | POSTGRES_USER: postgres
20 | POSTGRES_PASSWORD: postgres
21 | POSTGRES_HOST: postgres
22 | ports:
23 | - 5432/tcp
24 | options: >-
25 | --health-cmd pg_isready
26 | --health-interval 10s
27 | --health-timeout 5s
28 | --health-retries 5
29 |
30 | steps:
31 | - uses: actions/checkout@v2
32 | - name: Set up Python ${{ matrix.python-version }}
33 | uses: actions/setup-python@v2
34 | with:
35 | python-version: ${{ matrix.python-version }}
36 | - name: Install dependencies
37 | run: |
38 | python -m pip install --upgrade pip
39 | pip install -r tests/dev.requirements.txt
40 | - name: Test with pytest
41 | env:
42 | POSTGRES_DB: test
43 | POSTGRES_USER: postgres
44 | POSTGRES_PASSWORD: postgres
45 | POSTGRES_HOST: localhost
46 | POSTGRES_PORT: ${{ job.services.postgres.ports[5432] }}
47 | run: |
48 | pytest
49 |
--------------------------------------------------------------------------------
/.github/workflows/update-release-docs.yml:
--------------------------------------------------------------------------------
1 | name: Update Release Docs
2 | on: release
3 |
4 | jobs:
5 | Update:
6 | runs-on: ubuntu-latest
7 |
8 | steps:
9 | - uses: actions/checkout@v2
10 | - name: Set up Python
11 | uses: actions/setup-python@v2
12 | - name: Install dependencies
13 | run: |
14 | python -m pip install --upgrade pip
15 | pip install pygithub
16 | - name: Updating Releases Documentation
17 | env:
18 | GH_TOKEN: ${{ secrets.GH_TOKEN }}
19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
20 | run: |
21 | python .github/scripts/releases.py
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 | .vscode
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # Unit test / coverage reports
30 | htmlcov/
31 | .tox/
32 | .nox/
33 | .coverage
34 | .coverage.*
35 | .cache
36 | nosetests.xml
37 | coverage.xml
38 | *.cover
39 | *.py,cover
40 | .hypothesis/
41 | .pytest_cache/
42 | .mypy_cache/
43 |
44 | # Environments
45 | .env
46 | .venv
47 | env/
48 | venv/
49 | ENV/
50 | env.bak/
51 | venv.bak/
52 | p38venv/
53 |
54 | # Databases
55 | *.db
56 | db.sqlite3*
57 |
58 | # IDEs
59 | .idea
60 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | Pull requests and contributions are welcome. Please read the [contributions guidelines](https://fastapi-crudrouter.awtkns.com/contributing) for more details.
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Adam Watkins
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ⚡ Create CRUD routes with lighting speed ⚡
6 | A dynamic FastAPI router that automatically creates CRUD routes for your models
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | ---
21 |
22 | **Documentation**: https://fastapi-crudrouter.awtkns.com
23 |
24 | **Source Code**: https://github.com/awtkns/fastapi-crudrouter
25 |
26 | ---
27 | Tired of rewriting generic CRUD routes? Need to rapidly prototype a feature for a presentation
28 | or a hackathon? Thankfully, [fastapi-crudrouter](https://github.com/awtkns/fastapi-crudrouter) has your back. As an
29 | extension to the APIRouter included with [FastAPI](https://fastapi.tiangolo.com/), the FastAPI CRUDRouter will automatically
30 | generate and document your CRUD routes for you, all you have to do is pass your model and maybe your database connection.
31 |
32 | FastAPI-CRUDRouter is **lighting fast**, well tested, and **production ready**.
33 |
34 |
35 | ## Installation
36 | ```bash
37 | pip install fastapi-crudrouter
38 | ```
39 |
40 | ## Basic Usage
41 | Below is a simple example of what the CRUDRouter can do. In just ten lines of code, you can generate all
42 | the crud routes you need for any model. A full list of the routes generated can be found [here](https://fastapi-crudrouter.awtkns.com/routing).
43 |
44 | ```python
45 | from pydantic import BaseModel
46 | from fastapi import FastAPI
47 | from fastapi_crudrouter import MemoryCRUDRouter as CRUDRouter
48 |
49 | class Potato(BaseModel):
50 | id: int
51 | color: str
52 | mass: float
53 |
54 | app = FastAPI()
55 | app.include_router(CRUDRouter(schema=Potato))
56 | ```
57 |
58 | ## Advanced Usage
59 | fastapi-crudrouter provides a number of features that allow you to get the most out of your automatically generated CRUD
60 | routes. Listed below are some highlights.
61 |
62 | - Automatic Pagination ([docs](https://fastapi-crudrouter.awtkns.com/pagination/))
63 | - Ability to Provide Custom Create and Update Schemas ([docs](https://fastapi-crudrouter.awtkns.com/schemas/))
64 | - Dynamic Generation of Create and Update Schemas ([docs](https://fastapi-crudrouter.awtkns.com/schemas/))
65 | - Ability to Add, Customize, or Disable Specific Routes ([docs](https://fastapi-crudrouter.awtkns.com/routing/))
66 | - Native Support for FastAPI Dependency Injection ([docs](https://fastapi-crudrouter.awtkns.com/dependencies/))
67 |
68 | ## Supported Backends / ORMs
69 | fastapi-crudrouter currently supports a number of backends / ORMs. Listed below are the backends currently supported. This list will
70 | likely grow in future releases.
71 |
72 | - In Memory ([docs](https://fastapi-crudrouter.awtkns.com/backends/memory/))
73 | - SQLAlchemy ([docs](https://fastapi-crudrouter.awtkns.com/backends/sqlalchemy/))
74 | - Databases (async) ([docs](https://fastapi-crudrouter.awtkns.com/backends/async/))
75 | - Gino (async) ([docs](https://fastapi-crudrouter.awtkns.com/backends/gino.html))
76 | - Ormar (async) ([docs](https://fastapi-crudrouter.awtkns.com/backends/ormar/))
77 | - Tortoise ORM (async) ([docs](https://fastapi-crudrouter.awtkns.com/backends/tortoise/))
78 |
79 | ## OpenAPI Support
80 | By default, all routes generated by the CRUDRouter will be documented according to OpenAPI spec.
81 |
82 | Below are the default routes created by the CRUDRouter shown in the generated OpenAPI documentation.
83 |
84 | 
85 |
86 | The CRUDRouter is able to dynamically generate detailed documentation based on the models given to it.
87 |
88 | 
89 |
--------------------------------------------------------------------------------
/docs/en/docs/assets/RouteDetail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awtkns/fastapi-crudrouter/9b829865d85113a3f16f94c029502a9a584d47bb/docs/en/docs/assets/RouteDetail.png
--------------------------------------------------------------------------------
/docs/en/docs/assets/RouteOverview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awtkns/fastapi-crudrouter/9b829865d85113a3f16f94c029502a9a584d47bb/docs/en/docs/assets/RouteOverview.png
--------------------------------------------------------------------------------
/docs/en/docs/assets/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awtkns/fastapi-crudrouter/9b829865d85113a3f16f94c029502a9a584d47bb/docs/en/docs/assets/banner.png
--------------------------------------------------------------------------------
/docs/en/docs/assets/bolt-grad.svg:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
23 |
24 |
26 | image/svg+xml
27 |
29 |
30 |
31 |
32 |
33 |
36 |
40 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/docs/en/docs/assets/bolt.svg:
--------------------------------------------------------------------------------
1 |
2 |
13 |
15 |
17 |
18 |
20 | image/svg+xml
21 |
23 |
24 |
25 |
26 |
27 |
30 |
34 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/docs/en/docs/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awtkns/fastapi-crudrouter/9b829865d85113a3f16f94c029502a9a584d47bb/docs/en/docs/assets/logo.png
--------------------------------------------------------------------------------
/docs/en/docs/backends/async.md:
--------------------------------------------------------------------------------
1 | Asynchronous routes will be automatically generated when using the `DatabasesCRUDRouter`. To use it, you must pass a
2 | [pydantic](https://pydantic-docs.helpmanual.io/) model, your SQLAlchemy Table, and the databases database.
3 | This CRUDRouter is intended to be used with the python [Databases](https://www.encode.io/databases/) library. An example
4 | of how to use [Databases](https://www.encode.io/databases/) with FastAPI can be found both
5 | [here](https://fastapi.tiangolo.com/advanced/async-sql-databases/) and below.
6 |
7 | !!! warning
8 | To use the `DatabasesCRUDRouter`, Databases **and** SQLAlchemy must be first installed.
9 |
10 | ## Minimal Example
11 | Below is a minimal example assuming that you have already imported and created
12 | all the required models and database connections.
13 |
14 | ```python
15 | from fastapi_crudrouter import DatabasesCRUDRouter
16 | from fastapi import FastAPI
17 |
18 | app = FastAPI()
19 |
20 | router = DatabasesCRUDRouter(
21 | schema=MyPydanticModel,
22 | table=my_table,
23 | database=my_database
24 | )
25 | app.include_router(router)
26 | ```
27 |
28 | ## Full Example
29 |
30 | ```python
31 | import databases
32 | import sqlalchemy
33 |
34 | from pydantic import BaseModel
35 | from fastapi import FastAPI
36 | from fastapi_crudrouter import DatabasesCRUDRouter
37 |
38 | DATABASE_URL = "sqlite:///./test.db"
39 |
40 | database = databases.Database(DATABASE_URL)
41 | engine = sqlalchemy.create_engine(
42 | DATABASE_URL,
43 | connect_args={"check_same_thread": False}
44 | )
45 |
46 | metadata = sqlalchemy.MetaData()
47 | potatoes = sqlalchemy.Table(
48 | "potatoes",
49 | metadata,
50 | sqlalchemy.Column("id", sqlalchemy.Integer, primary_key=True),
51 | sqlalchemy.Column("thickness", sqlalchemy.Float),
52 | sqlalchemy.Column("mass", sqlalchemy.Float),
53 | sqlalchemy.Column("color", sqlalchemy.String),
54 | sqlalchemy.Column("type", sqlalchemy.String),
55 | )
56 | metadata.create_all(bind=engine)
57 |
58 |
59 | class PotatoCreate(BaseModel):
60 | thickness: float
61 | mass: float
62 | color: str
63 | type: str
64 |
65 |
66 | class Potato(PotatoCreate):
67 | id: int
68 |
69 |
70 | app = FastAPI()
71 |
72 |
73 | @app.on_event("startup")
74 | async def startup():
75 | await database.connect()
76 |
77 |
78 | @app.on_event("shutdown")
79 | async def shutdown():
80 | await database.disconnect()
81 |
82 |
83 | router = DatabasesCRUDRouter(
84 | schema=Potato,
85 | create_schema=PotatoCreate,
86 | table=potatoes,
87 | database=database
88 | )
89 | app.include_router(router)
90 | ```
--------------------------------------------------------------------------------
/docs/en/docs/backends/gino.md:
--------------------------------------------------------------------------------
1 | Asynchronous routes will be automatically generated when using the `GinoCRUDRouter`. To use it, you must pass a
2 | [pydantic](https://pydantic-docs.helpmanual.io/) model, your SQLAlchemy Table, and the databases database.
3 | This CRUDRouter is intended to be used with the python [Gino](https://python-gino.org/) library. An example
4 | of how to use [Gino](https://python-gino.org/) with FastAPI can be found both
5 | [here](https://python-gino.org/docs/en/1.0/tutorials/fastapi.html) and below.
6 |
7 | !!! warning
8 | To use the `GinoCRUDRouter`, Gino **and** SQLAlchemy must be first installed.
9 |
10 | ## Minimal Example
11 | Below is a minimal example assuming that you have already imported and created
12 | all the required models and database connections.
13 |
14 | ```python
15 | router = GinoCRUDRouter(
16 | schema=MyPydanticModel,
17 | db=db,
18 | db_model=MyModel
19 | )
20 | app.include_router(router)
21 | ```
22 |
--------------------------------------------------------------------------------
/docs/en/docs/backends/memory.md:
--------------------------------------------------------------------------------
1 | The `MemoryCRUDRouter` is the simplest usage of the CRUDRouters. To use it, simply pass a
2 | [pydantic](https://pydantic-docs.helpmanual.io/) model to it. As a database is not required, the `MemoryCRUDRouter` is
3 | well suited for rapid bootstrapping and prototyping.
4 |
5 | ## Usage
6 | ```python
7 | from pydantic import BaseModel
8 | from fastapi import FastAPI
9 | from fastapi_crudrouter import MemoryCRUDRouter
10 |
11 | class Potato(BaseModel):
12 | id: int
13 | color: str
14 | mass: float
15 |
16 | app = FastAPI()
17 | router = MemoryCRUDRouter(schema=Potato)
18 | app.include_router(router)
19 | ```
20 |
21 | !!! warning
22 | When using the `MemoryCRUDRouter`, the schema (model) passed to it must have the `id: int` property.
23 |
24 | !!! danger
25 | The storage for the `MemoryCRUDRouter` resides in memory, not a database. Hence, the data is not persistent. Be careful when using it beyond
26 | the rapid bootstrapping or prototyping phase.
--------------------------------------------------------------------------------
/docs/en/docs/backends/ormar.md:
--------------------------------------------------------------------------------
1 | When generating routes, the `OrmarCRUDRouter` will automatically tie into your database
2 | using your [ormar](https://collerek.github.io/ormar/) models. To use it, simply pass your ormar database model as the schema.
3 |
4 | ## Simple Example
5 |
6 | Below is an example assuming that you have already imported and created all the required
7 | models.
8 |
9 | ```python
10 | from fastapi_crudrouter import OrmarCRUDRouter
11 | from fastapi import FastAPI
12 |
13 | app = FastAPI()
14 |
15 | router = OrmarCRUDRouter(
16 | schema=MyOrmarModel,
17 | create_schema=Optional[MyPydanticCreateModel],
18 | update_schema=Optional[MyPydanticUpdateModel]
19 | )
20 |
21 | app.include_router(router)
22 | ```
23 |
24 | !!! note
25 | The `create_schema` should not include the *primary id* field as this be
26 | generated by the database.
27 |
28 | ## Full Example
29 |
30 | ```python
31 | # example.py
32 | import databases
33 | import ormar
34 | import sqlalchemy
35 | import uvicorn
36 | from fastapi import FastAPI
37 |
38 | from fastapi_crudrouter import OrmarCRUDRouter
39 |
40 | DATABASE_URL = "sqlite:///./test.db"
41 | database = databases.Database(DATABASE_URL)
42 | metadata = sqlalchemy.MetaData()
43 |
44 | app = FastAPI()
45 |
46 |
47 | @app.on_event("startup")
48 | async def startup():
49 | await database.connect()
50 |
51 |
52 | @app.on_event("shutdown")
53 | async def shutdown():
54 | await database.disconnect()
55 |
56 |
57 | class BaseMeta(ormar.ModelMeta):
58 | metadata = metadata
59 | database = database
60 |
61 |
62 | def _setup_database():
63 | # if you do not have the database run this once
64 | engine = sqlalchemy.create_engine(DATABASE_URL)
65 | metadata.drop_all(engine)
66 | metadata.create_all(engine)
67 | return engine, database
68 |
69 |
70 | class Potato(ormar.Model):
71 | class Meta(BaseMeta):
72 | pass
73 |
74 | id = ormar.Integer(primary_key=True)
75 | thickness = ormar.Float()
76 | mass = ormar.Float()
77 | color = ormar.String(max_length=255)
78 | type = ormar.String(max_length=255)
79 |
80 |
81 | app.include_router(
82 | OrmarCRUDRouter(
83 | schema=Potato,
84 | prefix="potato",
85 | )
86 | )
87 |
88 | if __name__ == "__main__":
89 | uvicorn.run("example:app", host="127.0.0.1", port=5000, log_level="info")
90 | ```
91 |
--------------------------------------------------------------------------------
/docs/en/docs/backends/sqlalchemy.md:
--------------------------------------------------------------------------------
1 | When generating routes, the `SQLAlchemyCRUDRouter` will automatically tie into
2 | your database using your [SQLAlchemy](https://www.sqlalchemy.org/) models. To use it, you must pass a
3 | [pydantic](https://pydantic-docs.helpmanual.io/) model, your SQLAlchemy model to it, and the
4 | database dependency.
5 |
6 | !!! warning
7 | To use the `SQLAlchemyCRUDRouter`, SQLAlchemy must be first installed.
8 |
9 | ## Simple Example
10 | Below is an example assuming that you have already imported and created all the required models.
11 |
12 | ```python
13 | from fastapi_crudrouter import SQLAlchemyCRUDRouter
14 | from fastapi import FastAPI
15 |
16 | app = FastAPI()
17 |
18 | router = SQLAlchemyCRUDRouter(
19 | schema=MyPydanticModel,
20 | create_schema=MyPydanticCreateModel,
21 | db_model=MyDBModel,
22 | db=get_db
23 | )
24 |
25 | app.include_router(router)
26 | ```
27 |
28 | !!! note
29 | The `create_schema` should not include the *primary id* field as this be generated by the database.
30 |
31 | ## Full Example
32 |
33 | ```python
34 | from sqlalchemy import Column, String, Float, Integer
35 | from sqlalchemy import create_engine
36 | from sqlalchemy.ext.declarative import declarative_base
37 | from sqlalchemy.orm import sessionmaker
38 |
39 | from pydantic import BaseModel
40 | from fastapi import FastAPI
41 | from fastapi_crudrouter import SQLAlchemyCRUDRouter
42 |
43 | app = FastAPI()
44 | engine = create_engine(
45 | "sqlite:///./app.db",
46 | connect_args={"check_same_thread": False}
47 | )
48 |
49 | SessionLocal = sessionmaker(
50 | autocommit=False,
51 | autoflush=False,
52 | bind=engine
53 | )
54 |
55 | Base = declarative_base()
56 |
57 |
58 | def get_db():
59 | session = SessionLocal()
60 | try:
61 | yield session
62 | session.commit()
63 | finally:
64 | session.close()
65 |
66 |
67 | class PotatoCreate(BaseModel):
68 | thickness: float
69 | mass: float
70 | color: str
71 | type: str
72 |
73 |
74 | class Potato(PotatoCreate):
75 | id: int
76 |
77 | class Config:
78 | orm_mode = True
79 |
80 |
81 | class PotatoModel(Base):
82 | __tablename__ = 'potatoes'
83 | id = Column(Integer, primary_key=True, index=True)
84 | thickness = Column(Float)
85 | mass = Column(Float)
86 | color = Column(String)
87 | type = Column(String)
88 |
89 |
90 | Base.metadata.create_all(bind=engine)
91 |
92 | router = SQLAlchemyCRUDRouter(
93 | schema=Potato,
94 | create_schema=PotatoCreate,
95 | db_model=PotatoModel,
96 | db=get_db,
97 | prefix='potato'
98 | )
99 |
100 | app.include_router(router)
101 | ```
--------------------------------------------------------------------------------
/docs/en/docs/backends/tortoise.md:
--------------------------------------------------------------------------------
1 | When generating routes, the `TortoiseCRUDRouter` will automatically tie into
2 | your database using your [Tortoise](https://tortoise-orm.readthedocs.io/en/latest/index.html) models. To use it, you must pass a
3 | [pydantic](https://pydantic-docs.helpmanual.io/) schema, your Tortoise database model to it, and register Tortoise ORM with your FastAPI App.
4 |
5 | !!! warning
6 | To use the `TortoiseCRUDRouter`, [Tortoise ORM](https://pypi.org/project/tortoise-orm/) must be first installed.
7 |
8 | !!! warning
9 | Tortoise ORM works on python versions 3.7 and higher, so if you want to use this backend, you would not be able to use `python 3.6`.
10 |
11 | ## Simple Example
12 | Below is an example assuming that you have already imported and created all the required models.
13 |
14 | ```python
15 | from fastapi_crudrouter.core.tortoise import TortoiseCRUDRouter
16 | from fastapi import FastAPI
17 |
18 | app = FastAPI()
19 | register_tortoise(app, config=TORTOISE_ORM)
20 |
21 | router = TortoiseCRUDRouter(
22 | schema=MyPydanticModel,
23 | db_model=MyDBModel,
24 | prefix="test"
25 | )
26 |
27 | app.include_router(router)
28 | ```
29 |
30 | You can provide your TORTOISE_ORM from a file or as a dictionary. If you want to provide it as a dictionary, this would be how:
31 |
32 | ```python
33 | TORTOISE_ORM = {
34 | "connections": {"default": 'postgres_url_here'},
35 | "apps": {
36 | "models": {
37 | "models": ["example"],
38 | "default_connection": "default",
39 | },
40 | },
41 | }
42 | ```
43 |
44 | Where `"models": ["example"]` represents that `example.py` is where your Tortoise database ORM models are located.
45 | If you ended up having a lot of these, you might want to break it out into a `models.py` file and thus your config would change to `"models": ["models"]`.
46 | If you use [Aerich](https://github.com/tortoise/aerich) for database migrations, you'll need to add `"aerich.models"` to your config.
47 |
48 | !!! note
49 | The `create_schema` should not include the *primary id* field as this should be generated by the database. If you don't provide a create_schema, a primary key stripped schema will be made for you.
50 |
51 | ## Full Example
52 |
53 | ```python
54 | # example.py
55 |
56 | import uvicorn as uvicorn
57 | from fastapi import FastAPI
58 | from fastapi_crudrouter.core.tortoise import TortoiseCRUDRouter
59 | from tortoise.contrib.fastapi import register_tortoise
60 | from tortoise.contrib.pydantic import pydantic_model_creator
61 | from tortoise.models import Model
62 | from tortoise import fields, Tortoise
63 |
64 | TORTOISE_ORM = {
65 | "connections": {"default": 'postgres_url'},
66 | "apps": {
67 | "models": {
68 | "models": ["example"],
69 | "default_connection": "default",
70 | },
71 | },
72 | }
73 |
74 | # Create Database Tables
75 | async def init():
76 | await Tortoise.init(config=TORTOISE_ORM)
77 | await Tortoise.generate_schemas()
78 |
79 | app = FastAPI()
80 | register_tortoise(app, config=TORTOISE_ORM)
81 |
82 |
83 | # Tortoise ORM Model
84 | class TestModel(Model):
85 | test = fields.IntField(null=False, description=f"Test value")
86 | ts = fields.IntField(null=False, description=f"Epoch time")
87 |
88 |
89 | # Pydantic schema
90 | TestSchema = pydantic_model_creator(TestModel, name=f"{TestModel.__name__}Schema")
91 | TestSchemaCreate = pydantic_model_creator(TestModel, name=f"{TestModel.__name__}SchemaCreate", exclude_readonly=True)
92 |
93 | # Make your FastAPI Router from your Pydantic schema and Tortoise Model
94 | router = TortoiseCRUDRouter(
95 | schema=TestSchema,
96 | create_schema=TestSchemaCreate,
97 | db_model=TestModel,
98 | prefix="test"
99 | )
100 |
101 | # Add it to your app
102 | app.include_router(router)
103 |
104 | if __name__ == "__main__":
105 | uvicorn.run("example:app", host="127.0.0.1", port=5000, log_level="info")
106 | ```
--------------------------------------------------------------------------------
/docs/en/docs/contributing.md:
--------------------------------------------------------------------------------
1 | As an open source package, fastapi-crudrouter accepts contributions from **all members** of the community. If you are interested in the
2 | contributing, reading the development guidelines below may help you in the process. 😊
3 |
4 | ## Github
5 |
6 | #### Issues
7 | Please create an issue to report a bug, request a feature or to simply ask a question.
8 |
9 |
10 | #### Pull Requests
11 | Unless the pull request is a simple bugfix, please try to create an issue before starting on the implementation of your pull request.
12 | This ensures that the potential feature is in alignment with CRUDRouter's goals moving forward. This also allows for feedback
13 | on the feature and potential help on where to start implementation wise.
14 |
15 | ## Development
16 |
17 | ### Installing the Dev Requirements
18 | FastAPI-Crudrouter requires as set of development requirements that can installed with `pip` be found in `tests/dev.requirements.txt`
19 |
20 |
21 |
22 | ```console
23 | $ pip install -r tests/dev.requirements.txt
24 | ---> 100%
25 | ```
26 |
27 |
28 |
29 | ### Testing
30 | When adding additional features, please try to add additional tests that prove that your implementation
31 | works and is bug free.
32 |
33 | #### Test requirements
34 | Tests require a postgres database for tests to run. The easiest way to accomplish this is with docker. This project offers
35 | a docker-compose file at tests/conf/dev.docker-compose.yml. You can use this file with
36 |
37 | ```bash
38 | docker compose -f tests/conf/dev.docker-compose.yml up -d
39 | ```
40 |
41 | After testing you can tear down the running containers with
42 |
43 | ```bash
44 | docker compose -f tests/conf/dev.docker-compose.yml down
45 | ```
46 |
47 | #### Running tests
48 | Crudrouter utilizes the [pytest](https://docs.pytest.org/en/latest/) framework for all of its unittests. Tests can be run
49 | as shown below.
50 |
51 |
52 |
53 | ```console
54 | $ pytest
55 | ---> 100%
56 | ```
57 |
58 |
59 |
60 | ### Linting, Formatting and Typing
61 |
62 | With `dev.requirements.txt` installed above you also install tools to lint, format and static type check the project.
63 |
64 | To format the project run:
65 |
66 | ```bash
67 | black fastapi_crudrouter tests
68 | ```
69 |
70 | To check styles, imports, annotations, pep8 etc. run:
71 |
72 | ```bash
73 | flake8 fastapi_crudrouter
74 | ```
75 |
76 | To check static types annotations run:
77 |
78 | ```bash
79 | mypy fastapi_crudrouter tests
80 | ```
81 |
82 | ### Documentation
83 | Crudrouter's documentation was built using [mkdocs-material](https://squidfunk.github.io/mkdocs-material/). To start the development
84 | documentation server, please first install mkdocs-material and then run the server as shown below.
85 |
86 |
87 |
88 | ```console
89 | $ pip install mkdocs-material
90 | ---> 100%
91 | $ cd docs/en
92 | $ mkdocs serve
93 | ```
94 |
95 |
96 |
--------------------------------------------------------------------------------
/docs/en/docs/css/main.css:
--------------------------------------------------------------------------------
1 | :root {
2 | /*--md-primary-fg-color: #3d3393;*/
3 | /*--md-primary-fg-color--light: #3d3393;*/
4 | /*--md-primary-fg-color--dark: #3d3393;*/
5 | }
6 |
7 | header {
8 | /* https://webgradients.com/ - 142 Space Shift */
9 | background-image: linear-gradient(60deg, #3d3393 0%, #2b76b9 37%, #2cacd1 65%, #35eb93 100%);
10 | }
11 |
12 | /* Sidebar */
13 | @media screen and (max-width: 76.1875em) {
14 | .md-nav__title {
15 | background-image: linear-gradient(60deg, #3d3393 0%, #2b76b9 37%, #2cacd1 65%, #35eb93 100%);
16 | }
17 |
18 | .md-nav--primary .md-nav__title {
19 | color: white;
20 | }
21 | }
22 |
23 | @media screen and (min-width: 60em) {
24 | .md-nav__title[for="__toc"] {
25 | background-image: None !important;
26 | }
27 | }
28 |
29 | a.announce:link, a.announce:visited {
30 | color: #fff;
31 | }
32 |
33 | a.announce:hover {
34 | color: var(--md-accent-fg-color);
35 | }
36 |
37 | h2.releases {
38 | margin-bottom: 0;
39 | }
40 |
41 | h2.releases + p {
42 | margin-top: 0;
43 | }
44 |
45 |
--------------------------------------------------------------------------------
/docs/en/docs/css/termynal.css:
--------------------------------------------------------------------------------
1 | /**
2 | * termynal.js
3 | *
4 | * @author Ines Montani
5 | * @version 0.0.1
6 | * @license MIT
7 | */
8 |
9 | :root {
10 | --color-bg: #252a33;
11 | --color-text: #eee;
12 | --color-text-subtle: #a2a2a2;
13 | }
14 |
15 | [data-termynal] {
16 | width: 750px;
17 | max-width: 100%;
18 | background: var(--color-bg);
19 | color: var(--color-text);
20 | font-size: 18px;
21 | /* font-family: 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; */
22 | font-family: 'Roboto Mono', 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace;
23 | border-radius: 4px;
24 | padding: 75px 45px 35px;
25 | position: relative;
26 | -webkit-box-sizing: border-box;
27 | box-sizing: border-box;
28 | }
29 |
30 | [data-termynal]:before {
31 | content: '';
32 | position: absolute;
33 | top: 15px;
34 | left: 15px;
35 | display: inline-block;
36 | width: 15px;
37 | height: 15px;
38 | border-radius: 50%;
39 | /* A little hack to display the window buttons in one pseudo element. */
40 | background: #d9515d;
41 | -webkit-box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930;
42 | box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930;
43 | }
44 |
45 | [data-termynal]:after {
46 | content: 'bash';
47 | position: absolute;
48 | color: var(--color-text-subtle);
49 | top: 5px;
50 | left: 0;
51 | width: 100%;
52 | text-align: center;
53 | }
54 |
55 | a[data-terminal-control] {
56 | text-align: right;
57 | display: block;
58 | color: #aebbff;
59 | }
60 |
61 | [data-ty] {
62 | display: block;
63 | line-height: 2;
64 | }
65 |
66 | [data-ty]:before {
67 | /* Set up defaults and ensure empty lines are displayed. */
68 | content: '';
69 | display: inline-block;
70 | vertical-align: middle;
71 | }
72 |
73 | [data-ty="input"]:before,
74 | [data-ty-prompt]:before {
75 | margin-right: 0.75em;
76 | color: var(--color-text-subtle);
77 | }
78 |
79 | [data-ty="input"]:before {
80 | content: '$';
81 | }
82 |
83 | [data-ty][data-ty-prompt]:before {
84 | content: attr(data-ty-prompt);
85 | }
86 |
87 | [data-ty-cursor]:after {
88 | content: attr(data-ty-cursor);
89 | font-family: monospace;
90 | margin-left: 0.5em;
91 | -webkit-animation: blink 1s infinite;
92 | animation: blink 1s infinite;
93 | }
94 |
95 |
96 | /* Cursor animation */
97 | @-webkit-keyframes blink {
98 | 50% {
99 | opacity: 0;
100 | }
101 | }
102 |
103 | @keyframes blink {
104 | 50% {
105 | opacity: 0;
106 | }
107 | }
--------------------------------------------------------------------------------
/docs/en/docs/dependencies.md:
--------------------------------------------------------------------------------
1 | All the CRUDRouters included with `fastapi_crudrouter` support FastAPI dependency injection.
2 |
3 | !!! tip
4 | Since all CRUDRouter's subclass the [FastAPI APIRouter](https://fastapi.tiangolo.com/tutorial/bigger-applications/?h=+router#apirouter),
5 | you can use any features APIRouter features.
6 |
7 | Below is a simple example of how you could use OAuth2 in conjunction with a CRUDRouter to secure your routes.
8 |
9 | ```python
10 | from fastapi import FastAPI, Depends, HTTPException
11 | from fastapi.security import OAuth2PasswordBearer
12 | from fastapi_crudrouter import MemoryCRUDRouter
13 |
14 | app = FastAPI()
15 | oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/token")
16 |
17 | def token_auth(token: str=Depends(oauth2_scheme)):
18 | if not token:
19 | raise HTTPException(401, "Invalid token")
20 |
21 | router = MemoryCRUDRouter(schema=MySchema, dependencies=[Depends(token_auth)])
22 | app.include_router(router)
23 | ```
24 |
25 | ## Custom Dependencies Per Route
26 | All CRUDRouters allow you to add a sequence of dependencies on a per-route basis. The dependencies can be set when
27 | initializing any CRUDRouter using the key word arguments below.
28 |
29 | ```python
30 | CRUDRouter(
31 | # ...
32 | get_all_route=[Depends(get_all_route_dep), ...],
33 | get_one_route=[Depends(get_one_route_dep), ...],
34 | create_route=[Depends(create_route_dep), ...],
35 | update_route=[Depends(update_route_dep), ...],
36 | delete_one_route=[Depends(user), ...],
37 | delete_all_route=[Depends(user), ...],
38 | )
39 |
40 | ```
41 |
42 | !!! tip "Multiple Dependencies Per Route"
43 | As they are passed as a sequence, you are able to set multiple dependencies individually per route.
44 |
45 | !!! attention "Disabling Routes Entirely"
46 | Setting the key word arguments shown above to `False`, disables the route entirely.
47 |
48 |
49 | ### Example
50 | In the example below, we are adding a fictitious dependency to the "create route" (POST) which requires the user to be
51 | logged in to create an object. At the same time, we are also independently adding an admin dependency to only the "delete all
52 | route" which limits the route's usage to only admin users.
53 |
54 | ```python
55 | MemoryCRUDRouter(
56 | schema=MySchema,
57 | create_route=[Depends(user)],
58 | delete_all_route=[Depends(admin)]
59 | )
60 | ```
61 |
62 |
63 |
--------------------------------------------------------------------------------
/docs/en/docs/index.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ⚡ Create CRUD routes with lighting speed ⚡
6 | A dynamic FastAPI router that automatically creates routes CRUD for your models
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | ---
21 |
22 | **Documentation**: https://fastapi-crudrouter.awtkns.com
23 |
24 | **Source Code**: https://github.com/awtkns/fastapi-crudrouter
25 |
26 | ---
27 | Tired of rewriting the same generic CRUD routes? Need to rapidly prototype a feature for a presentation
28 | or a hackathon? Thankfully, [fastapi-crudrouter](https://github.com/awtkns/fastapi-crudrouter) has your back. As an
29 | extension to the APIRouter included with [FastAPI](https://fastapi.tiangolo.com/), the FastAPI CRUDRouter will automatically
30 | generate and document your CRUD routes for you, all you have to do is pass your model and maybe your database connection.
31 |
32 | FastAPI-CRUDRouter is also **lightning fast**, well tested, and production ready.
33 |
34 | ## Installation
35 |
36 |
37 |
38 | ```console
39 | $ pip install fastapi-crudrouter
40 |
41 | ---> 100%
42 | ```
43 |
44 |
45 |
46 |
47 |
48 | ## Basic Usage
49 | Below is a simple example of what the CRUDRouter can do. In just ten lines of code, you can generate all
50 | the crud routes you need for any model. A full list of the routes generated can be found [here](./routing).
51 |
52 | ```python
53 | from pydantic import BaseModel
54 | from fastapi import FastAPI
55 | from fastapi_crudrouter import MemoryCRUDRouter as CRUDRouter
56 |
57 | class Potato(BaseModel):
58 | id: int
59 | color: str
60 | mass: float
61 |
62 | app = FastAPI()
63 | app.include_router(CRUDRouter(schema=Potato))
64 | ```
65 |
66 | ## Advanced Usage
67 | fastapi-crudrouter provides a number of features that allow you to get the most out of your automatically generated CRUD
68 | routes. Listed below are some highlights.
69 |
70 | - Automatic Pagination ([docs](https://fastapi-crudrouter.awtkns.com/pagination/))
71 | - Ability to Provide Custom Create and Update Schemas ([docs](https://fastapi-crudrouter.awtkns.com/schemas/))
72 | - Dynamic Generation of Create and Update Schemas ([docs](https://fastapi-crudrouter.awtkns.com/schemas/))
73 | - Ability to Add, Customize, or Disable Specific Routes ([docs](https://fastapi-crudrouter.awtkns.com/routing/))
74 | - Native Support for FastAPI Dependencies Injection ([docs](https://fastapi-crudrouter.awtkns.com/dependencies/))
75 |
76 | ## Supported Backends / ORMs
77 | fastapi-crudrouter supports a number of backends / ORMs. Listed below are the backends currently supported. This list will
78 | likely grow in future releases.
79 |
80 | - In Memory ([docs](https://fastapi-crudrouter.awtkns.com/backends/memory/))
81 | - SQLAlchemy ([docs](https://fastapi-crudrouter.awtkns.com/backends/sqlalchemy/))
82 | - Databases (async) ([docs](https://fastapi-crudrouter.awtkns.com/backends/async/))
83 | - Ormar (async) ([docs](https://fastapi-crudrouter.awtkns.com/backends/ormar/))
84 | - Gino (async) ([docs](https://fastapi-crudrouter.awtkns.com/backends/gino/))
85 | - Tortoise ORM (async) ([docs](https://fastapi-crudrouter.awtkns.com/backends/tortoise/))
86 |
87 | ## OpenAPI Support
88 |
89 | !!! tip "Automatic OpenAPI Documentation"
90 | By default, all routes generated by the CRUDRouter will be documented according to OpenAPI spec.
91 |
92 | Below are the default routes created by the CRUDRouter shown in the generated OpenAPI documentation.
93 |
94 | 
95 |
96 | The CRUDRouter is able to dynamically generate detailed documentation based on the models given to it.
97 |
98 | 
99 |
--------------------------------------------------------------------------------
/docs/en/docs/js/termy.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Ramirez aka Tiangalo
3 | * @license MIT
4 | */
5 |
6 | function setupTermynal() {
7 | document.querySelectorAll(".use-termynal").forEach(node => {
8 | node.style.display = "block";
9 | new Termynal(node, {
10 | lineDelay: 500
11 | });
12 | });
13 | const progressLiteralStart = "---> 100%";
14 | const promptLiteralStart = "$ ";
15 | const customPromptLiteralStart = "# ";
16 | const termynalActivateClass = "termy";
17 | let termynals = [];
18 |
19 | function createTermynals() {
20 | document
21 | .querySelectorAll(`.${termynalActivateClass} .highlight`)
22 | .forEach(node => {
23 | const text = node.textContent;
24 | const lines = text.split("\n");
25 | const useLines = [];
26 | let buffer = [];
27 | function saveBuffer() {
28 | if (buffer.length) {
29 | let isBlankSpace = true;
30 | buffer.forEach(line => {
31 | if (line) {
32 | isBlankSpace = false;
33 | }
34 | });
35 | dataValue = {};
36 | if (isBlankSpace) {
37 | dataValue["delay"] = 0;
38 | }
39 | if (buffer[buffer.length - 1] === "") {
40 | // A last single won't have effect
41 | // so put an additional one
42 | buffer.push("");
43 | }
44 | const bufferValue = buffer.join(" ");
45 | dataValue["value"] = bufferValue;
46 | useLines.push(dataValue);
47 | buffer = [];
48 | }
49 | }
50 | for (let line of lines) {
51 | if (line === progressLiteralStart) {
52 | saveBuffer();
53 | useLines.push({
54 | type: "progress"
55 | });
56 | } else if (line.startsWith(promptLiteralStart)) {
57 | saveBuffer();
58 | const value = line.replace(promptLiteralStart, "").trimEnd();
59 | useLines.push({
60 | type: "input",
61 | value: value
62 | });
63 | } else if (line.startsWith("// ")) {
64 | saveBuffer();
65 | const value = "💬 " + line.replace("// ", "").trimEnd();
66 | useLines.push({
67 | value: value,
68 | class: "termynal-comment",
69 | delay: 0
70 | });
71 | } else if (line.startsWith(customPromptLiteralStart)) {
72 | saveBuffer();
73 | const promptStart = line.indexOf(promptLiteralStart);
74 | if (promptStart === -1) {
75 | console.error("Custom prompt found but no end delimiter", line)
76 | }
77 | const prompt = line.slice(0, promptStart).replace(customPromptLiteralStart, "")
78 | let value = line.slice(promptStart + promptLiteralStart.length);
79 | useLines.push({
80 | type: "input",
81 | value: value,
82 | prompt: prompt
83 | });
84 | } else {
85 | buffer.push(line);
86 | }
87 | }
88 | saveBuffer();
89 | const div = document.createElement("div");
90 | node.replaceWith(div);
91 | const termynal = new Termynal(div, {
92 | lineData: useLines,
93 | noInit: true,
94 | lineDelay: 500
95 | });
96 | termynals.push(termynal);
97 | });
98 | }
99 |
100 | function loadVisibleTermynals() {
101 | termynals = termynals.filter(termynal => {
102 | if (termynal.container.getBoundingClientRect().top - innerHeight <= 0) {
103 | termynal.init();
104 | return false;
105 | }
106 | return true;
107 | });
108 | }
109 | window.addEventListener("scroll", loadVisibleTermynals);
110 | createTermynals();
111 | loadVisibleTermynals();
112 | }
113 |
114 | setupTermynal()
--------------------------------------------------------------------------------
/docs/en/docs/js/termynal.js:
--------------------------------------------------------------------------------
1 | /**
2 | * termynal.js
3 | * A lightweight, modern and extensible animated terminal window, using
4 | * async/await.
5 | *
6 | * @author Ines Montani
7 | * @version 0.0.1
8 | * @license MIT
9 | */
10 |
11 | 'use strict';
12 |
13 | /** Generate a terminal widget. */
14 | class Termynal {
15 | /**
16 | * Construct the widget's settings.
17 | * @param {(string|Node)=} container - Query selector or container element.
18 | * @param {Object=} options - Custom settings.
19 | * @param {string} options.prefix - Prefix to use for data attributes.
20 | * @param {number} options.startDelay - Delay before animation, in ms.
21 | * @param {number} options.typeDelay - Delay between each typed character, in ms.
22 | * @param {number} options.lineDelay - Delay between each line, in ms.
23 | * @param {number} options.progressLength - Number of characters displayed as progress bar.
24 | * @param {string} options.progressChar – Character to use for progress bar, defaults to █.
25 | * @param {number} options.progressPercent - Max percent of progress.
26 | * @param {string} options.cursor – Character to use for cursor, defaults to ▋.
27 | * @param {Object[]} lineData - Dynamically loaded line data objects.
28 | * @param {boolean} options.noInit - Don't initialise the animation.
29 | */
30 | constructor(container = '#termynal', options = {}) {
31 | this.container = (typeof container === 'string') ? document.querySelector(container) : container;
32 | this.pfx = `data-${options.prefix || 'ty'}`;
33 | this.originalStartDelay = this.startDelay = options.startDelay
34 | || parseFloat(this.container.getAttribute(`${this.pfx}-startDelay`)) || 600;
35 | this.originalTypeDelay = this.typeDelay = options.typeDelay
36 | || parseFloat(this.container.getAttribute(`${this.pfx}-typeDelay`)) || 90;
37 | this.originalLineDelay = this.lineDelay = options.lineDelay
38 | || parseFloat(this.container.getAttribute(`${this.pfx}-lineDelay`)) || 1500;
39 | this.progressLength = options.progressLength
40 | || parseFloat(this.container.getAttribute(`${this.pfx}-progressLength`)) || 40;
41 | this.progressChar = options.progressChar
42 | || this.container.getAttribute(`${this.pfx}-progressChar`) || '█';
43 | this.progressPercent = options.progressPercent
44 | || parseFloat(this.container.getAttribute(`${this.pfx}-progressPercent`)) || 100;
45 | this.cursor = options.cursor
46 | || this.container.getAttribute(`${this.pfx}-cursor`) || '▋';
47 | this.lineData = this.lineDataToElements(options.lineData || []);
48 | this.loadLines()
49 | if (!options.noInit) this.init()
50 | }
51 |
52 | loadLines() {
53 | // Load all the lines and create the container so that the size is fixed
54 | // Otherwise it would be changing and the user viewport would be constantly
55 | // moving as she/he scrolls
56 | const finish = this.generateFinish()
57 | finish.style.visibility = 'hidden'
58 | this.container.appendChild(finish)
59 | // Appends dynamically loaded lines to existing line elements.
60 | this.lines = [...this.container.querySelectorAll(`[${this.pfx}]`)].concat(this.lineData);
61 | for (let line of this.lines) {
62 | line.style.visibility = 'hidden'
63 | this.container.appendChild(line)
64 | }
65 | const restart = this.generateRestart()
66 | restart.style.visibility = 'hidden'
67 | this.container.appendChild(restart)
68 | this.container.setAttribute('data-termynal', '');
69 | }
70 |
71 | /**
72 | * Initialise the widget, get lines, clear container and start animation.
73 | */
74 | init() {
75 | /**
76 | * Calculates width and height of Termynal container.
77 | * If container is empty and lines are dynamically loaded, defaults to browser `auto` or CSS.
78 | */
79 | const containerStyle = getComputedStyle(this.container);
80 | this.container.style.width = containerStyle.width !== '0px' ?
81 | containerStyle.width : undefined;
82 | this.container.style.minHeight = containerStyle.height !== '0px' ?
83 | containerStyle.height : undefined;
84 |
85 | this.container.setAttribute('data-termynal', '');
86 | this.container.innerHTML = '';
87 | for (let line of this.lines) {
88 | line.style.visibility = 'visible'
89 | }
90 | this.start();
91 | }
92 |
93 | /**
94 | * Start the animation and rener the lines depending on their data attributes.
95 | */
96 | async start() {
97 | this.addFinish()
98 | await this._wait(this.startDelay);
99 |
100 | for (let line of this.lines) {
101 | const type = line.getAttribute(this.pfx);
102 | const delay = line.getAttribute(`${this.pfx}-delay`) || this.lineDelay;
103 |
104 | if (type == 'input') {
105 | line.setAttribute(`${this.pfx}-cursor`, this.cursor);
106 | await this.type(line);
107 | await this._wait(delay);
108 | }
109 |
110 | else if (type == 'progress') {
111 | await this.progress(line);
112 | await this._wait(delay);
113 | }
114 |
115 | else {
116 | this.container.appendChild(line);
117 | await this._wait(delay);
118 | }
119 |
120 | line.removeAttribute(`${this.pfx}-cursor`);
121 | }
122 | this.addRestart()
123 | this.finishElement.style.visibility = 'hidden'
124 | this.lineDelay = this.originalLineDelay
125 | this.typeDelay = this.originalTypeDelay
126 | this.startDelay = this.originalStartDelay
127 | }
128 |
129 | generateRestart() {
130 | const restart = document.createElement('a')
131 | restart.onclick = (e) => {
132 | e.preventDefault()
133 | this.container.innerHTML = ''
134 | this.init()
135 | }
136 | restart.href = '#'
137 | restart.setAttribute('data-terminal-control', '')
138 | restart.innerHTML = "restart ↻"
139 | return restart
140 | }
141 |
142 | generateFinish() {
143 | const finish = document.createElement('a')
144 | finish.onclick = (e) => {
145 | e.preventDefault()
146 | this.lineDelay = 0
147 | this.typeDelay = 0
148 | this.startDelay = 0
149 | }
150 | finish.href = '#'
151 | finish.setAttribute('data-terminal-control', '')
152 | finish.innerHTML = "fast →"
153 | this.finishElement = finish
154 | return finish
155 | }
156 |
157 | addRestart() {
158 | const restart = this.generateRestart()
159 | this.container.appendChild(restart)
160 | }
161 |
162 | addFinish() {
163 | const finish = this.generateFinish()
164 | this.container.appendChild(finish)
165 | }
166 |
167 | /**
168 | * Animate a typed line.
169 | * @param {Node} line - The line element to render.
170 | */
171 | async type(line) {
172 | const chars = [...line.textContent];
173 | line.textContent = '';
174 | this.container.appendChild(line);
175 |
176 | for (let char of chars) {
177 | const delay = line.getAttribute(`${this.pfx}-typeDelay`) || this.typeDelay;
178 | await this._wait(delay);
179 | line.textContent += char;
180 | }
181 | }
182 |
183 | /**
184 | * Animate a progress bar.
185 | * @param {Node} line - The line element to render.
186 | */
187 | async progress(line) {
188 | const progressLength = line.getAttribute(`${this.pfx}-progressLength`)
189 | || this.progressLength;
190 | const progressChar = line.getAttribute(`${this.pfx}-progressChar`)
191 | || this.progressChar;
192 | const chars = progressChar.repeat(progressLength);
193 | const progressPercent = line.getAttribute(`${this.pfx}-progressPercent`)
194 | || this.progressPercent;
195 | line.textContent = '';
196 | this.container.appendChild(line);
197 |
198 | for (let i = 1; i < chars.length + 1; i++) {
199 | await this._wait(this.typeDelay);
200 | const percent = Math.round(i / chars.length * 100);
201 | line.textContent = `${chars.slice(0, i)} ${percent}%`;
202 | if (percent>progressPercent) {
203 | break;
204 | }
205 | }
206 | }
207 |
208 | /**
209 | * Helper function for animation delays, called with `await`.
210 | * @param {number} time - Timeout, in ms.
211 | */
212 | _wait(time) {
213 | return new Promise(resolve => setTimeout(resolve, time));
214 | }
215 |
216 | /**
217 | * Converts line data objects into line elements.
218 | *
219 | * @param {Object[]} lineData - Dynamically loaded lines.
220 | * @param {Object} line - Line data object.
221 | * @returns {Element[]} - Array of line elements.
222 | */
223 | lineDataToElements(lineData) {
224 | return lineData.map(line => {
225 | let div = document.createElement('div');
226 | div.innerHTML = `${line.value || ''} `;
227 |
228 | return div.firstElementChild;
229 | });
230 | }
231 |
232 | /**
233 | * Helper function for generating attributes string.
234 | *
235 | * @param {Object} line - Line data object.
236 | * @returns {string} - String of attributes.
237 | */
238 | _attributes(line) {
239 | let attrs = '';
240 | for (let prop in line) {
241 | // Custom add class
242 | if (prop === 'class') {
243 | attrs += ` class=${line[prop]} `
244 | continue
245 | }
246 | if (prop === 'type') {
247 | attrs += `${this.pfx}="${line[prop]}" `
248 | } else if (prop !== 'value') {
249 | attrs += `${this.pfx}-${prop}="${line[prop]}" `
250 | }
251 | }
252 |
253 | return attrs;
254 | }
255 | }
256 |
257 | /**
258 | * HTML API: If current script has container(s) specified, initialise Termynal.
259 | */
260 | if (document.currentScript.hasAttribute('data-termynal-container')) {
261 | const containers = document.currentScript.getAttribute('data-termynal-container');
262 | containers.split('|')
263 | .forEach(container => new Termynal(container))
264 | }
--------------------------------------------------------------------------------
/docs/en/docs/pagination.md:
--------------------------------------------------------------------------------
1 | The CRUDRouter is set up to automatically paginate your routes for you. You can use the `skip` and `limit` query parameters to
2 | paginate your results.
3 |
4 | **Skip**:
5 | Using the `skip` (int) parameter, you can skip a certain number of items before returning the items you want.
6 |
7 | **Limit**:
8 | Using the `limit` (int) parameter, the maximum number of items to be returned can be defined.
9 |
10 | !!! tip "Setting a Maximum Pagination Limit"
11 | When creating a new CRUDRouter you are able to set the maximum amount of items that will be returned per page.
12 | To do this, use the `paginate` kwarg when creating a new CRUDRouter as shown in the example below.
13 |
14 | ```python
15 | CRUDRouter(
16 | schema=MyPydanticModel,
17 | paginate=25
18 | )
19 | ```
20 |
21 | Above a new CRUDRouter is being created that will paginate items at 25 items per page.
22 |
23 |
24 | ### Example
25 | Shown below is an example usage of pagination; using `skip` and `limit` to paginate results from the backend. More information on how to
26 | `skip` and `limit` can be used with fastapi can be found [here](https://fastapi.tiangolo.com/tutorial/sql-databases/#crud-utils).
27 |
28 | === "Python"
29 |
30 | ```python
31 | import requests
32 |
33 | requests.get('http://localhost:5000/potatoes' params={
34 | 'skip': 50,
35 | 'limit': 25
36 | })
37 | ```
38 |
39 | === "Bash"
40 |
41 | ```bash
42 | curl -X GET -G \
43 | 'http://localhost:5000/potatoes' \
44 | -d skip=50 \
45 | -d limit=25
46 | ```
47 |
48 | In the example above, 25 items on the third page are being returned from our fictitious CRUDRouter endpoint. It is the third
49 | page because we specified a `skip` of 50 items while having a `limit` of 25 items per page. If we were to want items on the fourth
50 | page we would simply have to increase the `skip` to 75.
51 |
52 |
53 |
54 | ### Validation
55 | CRUDRouter will return HTTP Validation error, status code 422, if any of these conditions are met:
56 |
57 | - The skip parameter is set to less than 0
58 | - The limit parameter is set to less than 1
59 | - The limit parameter is set to more than the maximum allowed number of records if a maximum is specified.
60 |
61 | Shown below is a sample validation error. In the example, a negative value for the `skip` parameter was supplied.
62 | ```json
63 | {
64 | "detail": {
65 | "detail": [
66 | {
67 | "loc": ["query", "skip"],
68 | "msg": "skip query parameter must be greater or equal to zero",
69 | "type": "type_error.integer"
70 | }
71 | ]
72 | }
73 | }
74 | ```
75 |
76 |
77 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/docs/en/docs/releases.md:
--------------------------------------------------------------------------------
1 | Release Notes
2 | ===
3 |
4 | ## [v0.8.5 - Typing](https://github.com/awtkns/fastapi-crudrouter/releases/tag/v0.8.5) { .releases }
5 | 2022-01-27
6 | ### 🎉 Highlights
7 | With the release of v0.8.5 fastapi-crudrouter now officially supports both **Python 3.10** and **typed python**. This release also includes significant improvements to the documentation, test suite, and developer experience.
8 |
9 | Keep an eye of for the next release which will contain support for **async SQLAlchemy** ([#122](https://github.com/awtkns/fastapi-crudrouter/pull/122)).
10 |
11 | Big thanks to all contributors that made this possible!
12 |
13 | ### ✨ Features
14 | - Typed python support [#132](https://github.com/awtkns/fastapi-crudrouter/pull/132) [#111](https://github.com/awtkns/fastapi-crudrouter/pull/111)
15 | - Python 3.10 support [#114](https://github.com/awtkns/fastapi-crudrouter/pull/114)
16 | - Test suite now runs against multiple databases [#86](https://github.com/awtkns/fastapi-crudrouter/pull/86)
17 | - Documentation improvements [#79](https://github.com/awtkns/fastapi-crudrouter/pull/79) [#91](https://github.com/awtkns/fastapi-crudrouter/pull/91) [#117](https://github.com/awtkns/fastapi-crudrouter/pull/117) [#123](https://github.com/awtkns/fastapi-crudrouter/pull/123) [#124](https://github.com/awtkns/fastapi-crudrouter/pull/124) [#125](https://github.com/awtkns/fastapi-crudrouter/pull/125) [@andrewthetechie](https://github.com/andrewthetechie)
18 | - More informative exceptions [#94](https://github.com/awtkns/fastapi-crudrouter/pull/94) [#137](https://github.com/awtkns/fastapi-crudrouter/pull/137)
19 | - General test suite improvements [#96](https://github.com/awtkns/fastapi-crudrouter/pull/96) [#97](https://github.com/awtkns/fastapi-crudrouter/pull/97)
20 |
21 | ### 🐛 Bug Fixes
22 | - OrderBy not working correctly with Microsoft SQL Server [#88](https://github.com/awtkns/fastapi-crudrouter/pull/88)
23 | - 404 response not documented in OpenAPI spec [#104](https://github.com/awtkns/fastapi-crudrouter/pull/104) [@sondrelg](https://github.com/sondrelg)
24 | - DatabasesCRUDRouter not functioning for inserts and deletes with AsyncPG [#98](https://github.com/awtkns/fastapi-crudrouter/pull/98)
25 |
26 | **Full Changelog**: [`v0.8.0...v0.8.5`](https://github.com/awtkns/fastapi-crudrouter/compare/v0.8.0...v0.8.5)
27 |
28 | ---
29 |
30 | ## [v0.8.0 - Gino Backend](https://github.com/awtkns/fastapi-crudrouter/releases/tag/v0.8.0) { .releases }
31 | 2021-07-06
32 | ### 🎉 Highlights
33 | With the release of v0.6.0 fastapi-crudrouter **now supports [Gino](https://github.com/python-gino/gino)** as an async backend! When generating routes, GinoCRUDRouter will automatically tie into your database using your Gino models. To use it, simply pass your Gino database model, a database reference, and your pydantic.
34 |
35 | ```python
36 | GinoCRUDRouter(
37 | schema=MyPydanticModel,
38 | db_model=MyModel,
39 | db=db
40 | )
41 | ```
42 |
43 | Check out the [docs](https://fastapi-crudrouter.awtkns.com/backends/gino.html) for more details on how to use the GinoCRUDRouter.
44 |
45 | ### ✨ Features
46 | - Full Gino Support [@Turall](https://github.com/Turall) [#78](https://github.com/awtkns/fastapi-crudrouter/pull/78)
47 | - Documentation improvements [#69](https://github.com/awtkns/fastapi-crudrouter/pull/69) [#75](https://github.com/awtkns/fastapi-crudrouter/pull/75)
48 |
49 | ### 🐛 Bug Fixes
50 | - All Path Prefixes are now correctly lowercase [#64](https://github.com/awtkns/fastapi-crudrouter/pull/64) [#65](https://github.com/awtkns/fastapi-crudrouter/pull/65)
51 |
52 |
53 | ---
54 |
55 | ## [v0.7.0 - Advanced Dependencies ](https://github.com/awtkns/fastapi-crudrouter/releases/tag/v0.7.0) { .releases }
56 | 2021-04-18
57 | ### 🎉 Highlights
58 | With the release of v0.7.0 fastapi-crudrouter now provides the ability to set custom dependencies on a per route basis; a much requested feature. Prior to this release, it was only possible to set dependencies for all the routes at once.
59 |
60 | ```python
61 | MemoryCRUDRouter(
62 | schema=MySchema,
63 | create_route=[Depends(user)],
64 | delete_all_route=[Depends(admin)]
65 | )
66 | ```
67 |
68 | Shown above is a brief example on how limiting each route to specific access rights would work using this new feature. Check out the [docs](https://fastapi-crudrouter.awtkns.com/dependencies/) for more details.
69 |
70 | ### ✨ Features
71 | - Custom Dependencies Per Route [#37](https://github.com/awtkns/fastapi-crudrouter/pull/37) [#59](https://github.com/awtkns/fastapi-crudrouter/pull/59) [#60](https://github.com/awtkns/fastapi-crudrouter/pull/60) [@DorskFR](https://github.com/DorskFR) [@jm-moreau](https://github.com/jm-moreau)
72 | - Ability to Provide a List of Custom Tags for OpenAPI [#57](https://github.com/awtkns/fastapi-crudrouter/pull/57) [@jm-moreau](https://github.com/jm-moreau)
73 | - Improved Documentation [#52](https://github.com/awtkns/fastapi-crudrouter/pull/52)
74 | - Dark Mode for Documentation
75 |
76 | ---
77 |
78 | ## [v0.6.0 - Ormar Backend](https://github.com/awtkns/fastapi-crudrouter/releases/tag/v0.6.0) { .releases }
79 | 2021-03-26
80 | ### 🎉 Highlights
81 | With the release of v0.6.0 fastapi-crudrouter **now supports [ormar](https://github.com/collerek/ormar)** as an async backend! When generating routes, the OrmarCRUDRouter will automatically tie into your database using your ormar models. To use it, simply pass your ormar database model as the schema.
82 |
83 | ```python
84 | OrmarCRUDRouter(
85 | schema=MyPydanticModel,
86 | paginate=25
87 | )
88 | ```
89 |
90 | Check out the [docs](https://fastapi-crudrouter.awtkns.com/backends/ormar/) for more details on how to use the `OrmarCRUDRouter`.
91 |
92 | ### ✨ Features
93 | - Full Ormar Support [@collerek](https://github.com/collerek) [#46](https://github.com/awtkns/fastapi-crudrouter/pull/46)
94 | - Better handling of database errors in the update route [@sorXCode](https://github.com/sorXCode) [#48](https://github.com/awtkns/fastapi-crudrouter/pull/48)
95 | - Improved typing [#46](https://github.com/awtkns/fastapi-crudrouter/pull/46) [#43](https://github.com/awtkns/fastapi-crudrouter/pull/43)
96 | - Black, Flake8 and Mypy linting [#46](https://github.com/awtkns/fastapi-crudrouter/pull/46)
97 | - Additional Tests for nested models [#40](https://github.com/awtkns/fastapi-crudrouter/pull/40)
98 |
99 | ### 🐛 Bug Fixes
100 | - Pagination issues when max limit was set to null [@ethanhaid](https://github.com/ethanhaid) [#42](https://github.com/awtkns/fastapi-crudrouter/pull/42)
101 |
102 | ---
103 |
104 | ## [v0.5.0 - Pagination](https://github.com/awtkns/fastapi-crudrouter/releases/tag/v0.5.0) { .releases }
105 | 2021-03-07
106 | ### 🎉 Highlights
107 | With the release of v0.5.0 all CRUDRouters **now supports pagination**. All "get all" routes now accept `skip` and `limit` query parameters allowing you to easily paginate your routes. By default, no limit is set on the number of items returned by your routes. Should you wish to limit the number of items that a client can request, it can be done as shown below.
108 |
109 | ```python
110 | CRUDRouter(
111 | schema=MyPydanticModel,
112 | paginate=25
113 | )
114 | ```
115 |
116 | Check out the [docs](https://fastapi-crudrouter.awtkns.com/pagination/) on pagination for more information!
117 |
118 | ### ✨ Features
119 | - Pagination Support [#34](https://github.com/awtkns/fastapi-crudrouter/pull/34)
120 | - Ability to set custom update schemas [@andreipopovici](https://github.com/andreipopovici) [#31](https://github.com/awtkns/fastapi-crudrouter/pull/31) [#27](https://github.com/awtkns/fastapi-crudrouter/pull/27)
121 | - Better documentation of past releases [#36](https://github.com/awtkns/fastapi-crudrouter/pull/36)
122 |
123 | ### 🐛 Bug Fixes
124 | - Prefixing not available for versions of fastapi below v0.62.0 [#29](https://github.com/awtkns/fastapi-crudrouter/pull/29) [#30](https://github.com/awtkns/fastapi-crudrouter/pull/30)
125 | - Fixed an Import Issue SQLAlchemy and Integrity Errors [@andreipopovici](https://github.com/andreipopovici) [#33](https://github.com/awtkns/fastapi-crudrouter/pull/33)
126 |
127 | ---
128 |
129 | ## [v0.4.0 - Tortoise ORM Support](https://github.com/awtkns/fastapi-crudrouter/releases/tag/v0.4.0) { .releases }
130 | 2021-02-02
131 | ### ✨Features
132 | - Full support for tortoise-orm [#24](https://github.com/awtkns/fastapi-crudrouter/pull/24)
133 | - Dynamic pk/id types for get_one, delete_one, and update_one routes [#26](https://github.com/awtkns/fastapi-crudrouter/pull/26)
134 |
135 | ### 🐛 Bug Fixes
136 | - Fixed the summary for the delete one route [#16](https://github.com/awtkns/fastapi-crudrouter/pull/16)
137 | - Fixed import errors when certain packages are not installed [#21](https://github.com/awtkns/fastapi-crudrouter/pull/21)
138 | - Improved SQLA type hinting
139 |
140 | ---
141 |
142 | ## [v0.3.0 - Initial Release](https://github.com/awtkns/fastapi-crudrouter/releases/tag/v0.3.0) { .releases }
143 | 2021-01-04
144 |
145 |
146 |
147 |
148 | 🎉 Initial Release 🎉
149 |
150 |
151 | Tired of rewriting the same generic CRUD routes? Need to rapidly prototype a feature for a presentation or a hackathon? Thankfully, fastapi-crudrouter has your back. As an extension to the APIRouter included with FastAPI, the FastAPI CRUDRouter will automatically generate and document your CRUD routes for you.
152 |
153 | **Documentation**: https://fastapi-crudrouter.awtkns.com
154 |
155 | **Source Code**: https://github.com/awtkns/fastapi-crudrouter
156 |
157 |
158 | ### Installation
159 | ```python
160 | pip install fastapi_crudrouter
161 | ```
162 |
163 | ### Usage
164 | Below is a simple example of what the CRUDRouter can do. In just ten lines of code, you can generate all the crud routes you need for any model. A full list of the routes generated can be found here.
165 | ```python
166 | from pydantic import BaseModel
167 | from fastapi import FastAPI
168 | from fastapi_crudrouter import MemoryCRUDRouter as CRUDRouter
169 |
170 | class Potato(BaseModel):
171 | id: int
172 | color: str
173 | mass: float
174 |
175 | app = FastAPI()
176 | app.include_router(CRUDRouter(model=Potato))
177 | ```
178 |
179 | ### Features
180 | - Automatic pydantic model based route generation and documentation ([Docs](https://fastapi-crudrouter.awtkns.com/routing/))
181 | - Ability to customize any of the generated routes ([Docs](https://fastapi-crudrouter.awtkns.com/routing/#overriding-routes))
182 | - Authorization and FastAPI dependency support ([Docs](https://fastapi-crudrouter.awtkns.com/dependencies/))
183 | - Support for both async and non-async relational databases using SQLAlchemy ([Docs](https://fastapi-crudrouter.awtkns.com/backends/sqlalchemy/))
184 | - Extensive documentation.
185 | - And much more 😎
--------------------------------------------------------------------------------
/docs/en/docs/routing.md:
--------------------------------------------------------------------------------
1 | Automatic route generation is the meat and potatoes of CRUDRouter's features. Detail below is how you can prefix, customize,
2 | and disable any routes generated by the CRUDRouter.
3 |
4 | ## Default Routes
5 | By default, the CRUDRouter will generate the six routes below for you.
6 |
7 | | Route | Method | Description
8 | | ------------ | -------- | ----
9 | | `/` | `GET` | Get all the resources
10 | | `/` | `POST` | Create a new resource
11 | | `/` | `DELETE` | Delete all the resources
12 | | `/{item_id}` | `GET` | Get an existing resource matching the given `item_id`
13 | | `/{item_id}` | `PUT` | Update an existing resource matching the given `item_id`
14 | | `/{item_id}` | `DELETE` | Delete an existing resource matching the given `item_id`
15 |
16 | !!! note "Route URLs"
17 | Note that the route url is prefixed by the defined prefix.
18 |
19 | **Example:** If the CRUDRouter's prefix is set as *potato* and I want to update a specific potato the route I want to access is
20 | `/potato/my_potato_id` where *my_potato_id* is the ID of the potato.
21 |
22 | ## Prefixes
23 | Depending on which CRUDRouter you are using, the CRUDRouter will try to automatically generate a suitable prefix for your
24 | model. By default, the [MemoryCRUDRouter](backends/memory.md) will use the pydantic model's name as the prefix. However,
25 | the [SQLAlchemyCRUDRouter](backends/sqlalchemy.md) will use the model's table name as the prefix.
26 |
27 | !!! tip "Custom Prefixes"
28 | You are also able to set custom prefixes with the `prefix` kwarg when creating your CRUDRouter. This can be done like so:
29 | `router = CRUDRouter(model=mymodel, prefix='carrot')`
30 |
31 | ## Disabling Routes
32 | Routes can be disabled from generating with a key word argument (kwarg) when creating your CRUDRouter. The valid kwargs
33 | are shown below.
34 |
35 | | Argument | Default | Description
36 | | ---------------- | ------ | ---
37 | | get_all_route | True | Setting this to false will prevent the get all route from generating
38 | | get_one_route | True | Setting this to false will prevent the get one route from generating
39 | | delete_all_route | True | Setting this to false will prevent the delete all route from generating
40 | | delete_one_route | True | Setting this to false will prevent the delete one route from generating
41 | | create_route | True | Setting this to false will prevent the create route from generating
42 | | update_route | True | Setting this to false will prevent the update route from generating
43 |
44 | As an example, the *delete all* route can be disabled by doing the following:
45 | ```python
46 | router = MemoryCRUDRouter(schema=MyModel, delete_all_route=False)
47 | ```
48 |
49 | !!! tip "Custom Dependencies"
50 | Instead to passing a bool to the arguments listed about, you can also pass a sequence of custom dependencies to be
51 | applied to each route. See the docs on [dependencies](dependencies.md) for more details.
52 |
53 |
54 | ## Overriding Routes
55 | Should you need to add custom functionality to any of your routes any of the included routers allows you to do so.
56 | Should you wish to disable a route from being generated, it can be done [here](../routing/#disabling-routes).
57 |
58 | Routes in the CRUDRouter can be overridden by using the standard fastapi route decorators. These include:
59 |
60 | - `@router.get(path: str, *args, **kwargs)`
61 | - `@router.post(path: str, *args, **kwargs)`
62 | - `@router.put(path: str, *args, **kwargs)`
63 | - `@router.delete(path: str, *args, **kwargs)`
64 | - `@router.api_route(path: str, methods: List[str] = ['GET'], *args, **kwargs)`
65 |
66 | !!! tip
67 | All of CRUDRouter's are a subclass of fastapi's [APIRouter](https://fastapi.tiangolo.com/tutorial/bigger-applications/#apirouter)
68 | meaning that they can be customized to your heart's content.
69 |
70 | ### Overriding Example
71 | Below is an example where we are overriding the routes `/potato/{item_id}` and `/potato` while using the MemoryCRUDRouter.
72 |
73 | ```python
74 | from pydantic import BaseModel
75 | from fastapi import FastAPI
76 | from fastapi_crudrouter import MemoryCRUDRouter as CRUDRouter
77 |
78 | class Potato(BaseModel):
79 | id: int
80 | color: str
81 | mass: float
82 |
83 | app = FastAPI()
84 | router = CRUDRouter(schema=Potato)
85 |
86 | @router.get('')
87 | def overloaded_get_all():
88 | return 'My overloaded route that returns all the items'
89 |
90 | @router.get('/{item_id}')
91 | def overloaded_get_one():
92 | return 'My overloaded route that returns one item'
93 |
94 | app.include_router(router)
95 | ```
96 |
--------------------------------------------------------------------------------
/docs/en/docs/schemas.md:
--------------------------------------------------------------------------------
1 | The CRUDRouter is able to generate and document your routes based on the schema, a
2 | [pydantic model](https://pydantic-docs.helpmanual.io/usage/models/#basic-model-usage), that is passed to it.
3 |
4 | In general, the all provided CRUDRouter's have the option to pass both a `schema` and a `create` schema to it. If no
5 | create schema is provided, the CRUDRouter will automatically generate one. Optionally you can also pass an `update` schema
6 | allowing for custom update behavior.
7 |
8 | ```python
9 | CRUDRouter(
10 | schema=MyPydanticModel,
11 | create_schema=MyPydanticCreateModel,
12 | update_schema=MyPydanticUpdateModel
13 | )
14 | ```
15 |
16 | !!! tip "Automatic Create and Update Schema Generation"
17 | Leaving the create and/or update schema argument blank when creating your CRUDRouter will result in the crud router automatically
18 | generating and documenting a create and/or update schema in your routes. In doing so, it automatically removes any field which
19 | matches the primary key in the database as this will be generated server side.
20 |
21 | ## Create Schemas
22 | Create schemas are models which typically don't include fields that are generated by a database or other backends. An example of this
23 | is an `id` field in a model.
24 |
25 | ```python
26 | from pydantic import BaseModel
27 |
28 | class Potato(BaseModel):
29 | id: int
30 | color: str
31 | mass: float
32 |
33 | class CreatePotato(BaseModel):
34 | color: str
35 | mass: float
36 | ```
37 |
38 | ## Update Schemas
39 | Update schemas allow you to limit which fields can be updated. As an example, the update schema below will only allow you to
40 | update the `color` field when used in the CRUDRouter.
41 |
42 | ```python
43 | from pydantic import BaseModel
44 |
45 | class Potato(BaseModel):
46 | id: int
47 | color: str
48 | mass: float
49 |
50 | # Allowing the user to only update the color
51 | class UpdatePotato(BaseModel):
52 | color: str
53 | ```
54 |
--------------------------------------------------------------------------------
/docs/en/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: FastAPI CRUD Router
2 | repo_url: https://github.com/awtkns/fastapi-crudrouter
3 | repo_name: awtkns/fastapi-crudrouter
4 | edit_uri: ''
5 |
6 | theme:
7 | name: material
8 | features:
9 | - navigation.expand
10 | - content.code.copy
11 | palette:
12 | - scheme: default
13 | primary: indigo
14 | accent: amber
15 | toggle:
16 | icon: material/weather-sunny
17 | name: Switch to dark mode
18 | - scheme: slate
19 | primary: blue
20 | accent: amber
21 | toggle:
22 | icon: material/weather-night
23 | name: Switch to light mode
24 | icon:
25 | repo: fontawesome/brands/github-alt
26 | logo: assets/bolt.svg
27 | favicon: assets/bolt-grad.svg
28 | language: en
29 |
30 | nav:
31 | - Overview: index.md
32 | - Schemas: schemas.md
33 | - Backends:
34 | - In Memory: backends/memory.md
35 | - SQLAlchemy: backends/sqlalchemy.md
36 | - Databases (async): backends/async.md
37 | - Gino (async): backends/gino.md
38 | - Ormar (async): backends/ormar.md
39 | - Tortoise (async): backends/tortoise.md
40 | - Routing: routing.md
41 | - Pagination: pagination.md
42 | - Dependencies: dependencies.md
43 | - Contributing: contributing.md
44 | - Releases: releases.md
45 |
46 | markdown_extensions:
47 | - extra
48 | - admonition
49 | - pymdownx.highlight
50 | - pymdownx.inlinehilite
51 | - pymdownx.superfences
52 | - pymdownx.tabbed
53 |
54 | extra:
55 | analytics:
56 | provider: google
57 | property: UA-186315536-1
58 | social:
59 | - icon: fontawesome/brands/github-alt
60 | link: https://github.com/awtkns/fastapi-crudrouter
61 | - icon: fontawesome/brands/linkedin
62 | link: https://www.linkedin.com/in/awtkns/
63 | - icon: fontawesome/solid/globe
64 | link: https://github.com/awtkns
65 |
66 | extra_css:
67 | - css/main.css
68 | - css/termynal.css
69 | extra_javascript:
70 | - js/termynal.js
71 | - js/termy.js
72 |
--------------------------------------------------------------------------------
/docs/en/overrides/main.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block announce %}
4 |
5 | 🎉 v0.7.0 Released - Advanced Dependency Support 🎉
6 |
7 | {% endblock %}
8 |
--------------------------------------------------------------------------------
/docs/en/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "cleanUrls": true,
3 | "trailingSlash": false,
4 | "redirects": [
5 | { "source": "/backends", "destination": "/" },
6 | { "source": "/memory", "destination": "backends/memory" },
7 | { "source": "/sqlalchemy", "destination": "backends/sqlalchemy" },
8 | { "source": "/async", "destination": "backends/async" },
9 | { "source": "/gino", "destination": "backends/gino" },
10 | { "source": "/ormar", "destination": "backends/ormar" },
11 | { "source": "/tortoise", "destination": "backends/tortoise" }
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/fastapi_crudrouter/__init__.py:
--------------------------------------------------------------------------------
1 | from .core import (
2 | DatabasesCRUDRouter,
3 | GinoCRUDRouter,
4 | MemoryCRUDRouter,
5 | OrmarCRUDRouter,
6 | SQLAlchemyCRUDRouter,
7 | TortoiseCRUDRouter,
8 | )
9 |
10 | from ._version import __version__ # noqa: F401
11 |
12 | __all__ = [
13 | "MemoryCRUDRouter",
14 | "SQLAlchemyCRUDRouter",
15 | "DatabasesCRUDRouter",
16 | "TortoiseCRUDRouter",
17 | "OrmarCRUDRouter",
18 | "GinoCRUDRouter",
19 | ]
20 |
--------------------------------------------------------------------------------
/fastapi_crudrouter/_version.py:
--------------------------------------------------------------------------------
1 | __version__ = "0.8.6"
2 |
--------------------------------------------------------------------------------
/fastapi_crudrouter/core/__init__.py:
--------------------------------------------------------------------------------
1 | from . import _utils
2 | from ._base import NOT_FOUND, CRUDGenerator
3 | from .databases import DatabasesCRUDRouter
4 | from .gino_starlette import GinoCRUDRouter
5 | from .mem import MemoryCRUDRouter
6 | from .ormar import OrmarCRUDRouter
7 | from .sqlalchemy import SQLAlchemyCRUDRouter
8 | from .tortoise import TortoiseCRUDRouter
9 |
10 | __all__ = [
11 | "_utils",
12 | "CRUDGenerator",
13 | "NOT_FOUND",
14 | "MemoryCRUDRouter",
15 | "SQLAlchemyCRUDRouter",
16 | "DatabasesCRUDRouter",
17 | "TortoiseCRUDRouter",
18 | "OrmarCRUDRouter",
19 | "GinoCRUDRouter",
20 | ]
21 |
--------------------------------------------------------------------------------
/fastapi_crudrouter/core/_base.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 | from typing import Any, Callable, Generic, List, Optional, Type, Union
3 |
4 | from fastapi import APIRouter, HTTPException
5 | from fastapi.types import DecoratedCallable
6 |
7 | from ._types import T, DEPENDENCIES
8 | from ._utils import pagination_factory, schema_factory
9 |
10 | NOT_FOUND = HTTPException(404, "Item not found")
11 |
12 |
13 | class CRUDGenerator(Generic[T], APIRouter, ABC):
14 | schema: Type[T]
15 | create_schema: Type[T]
16 | update_schema: Type[T]
17 | _base_path: str = "/"
18 |
19 | def __init__(
20 | self,
21 | schema: Type[T],
22 | create_schema: Optional[Type[T]] = None,
23 | update_schema: Optional[Type[T]] = None,
24 | prefix: Optional[str] = None,
25 | tags: Optional[List[str]] = None,
26 | paginate: Optional[int] = None,
27 | get_all_route: Union[bool, DEPENDENCIES] = True,
28 | get_one_route: Union[bool, DEPENDENCIES] = True,
29 | create_route: Union[bool, DEPENDENCIES] = True,
30 | update_route: Union[bool, DEPENDENCIES] = True,
31 | delete_one_route: Union[bool, DEPENDENCIES] = True,
32 | delete_all_route: Union[bool, DEPENDENCIES] = True,
33 | **kwargs: Any,
34 | ) -> None:
35 |
36 | self.schema = schema
37 | self.pagination = pagination_factory(max_limit=paginate)
38 | self._pk: str = self._pk if hasattr(self, "_pk") else "id"
39 | self.create_schema = (
40 | create_schema
41 | if create_schema
42 | else schema_factory(self.schema, pk_field_name=self._pk, name="Create")
43 | )
44 | self.update_schema = (
45 | update_schema
46 | if update_schema
47 | else schema_factory(self.schema, pk_field_name=self._pk, name="Update")
48 | )
49 |
50 | prefix = str(prefix if prefix else self.schema.__name__).lower()
51 | prefix = self._base_path + prefix.strip("/")
52 | tags = tags or [prefix.strip("/").capitalize()]
53 |
54 | super().__init__(prefix=prefix, tags=tags, **kwargs)
55 |
56 | if get_all_route:
57 | self._add_api_route(
58 | "",
59 | self._get_all(),
60 | methods=["GET"],
61 | response_model=Optional[List[self.schema]], # type: ignore
62 | summary="Get All",
63 | dependencies=get_all_route,
64 | )
65 |
66 | if create_route:
67 | self._add_api_route(
68 | "",
69 | self._create(),
70 | methods=["POST"],
71 | response_model=self.schema,
72 | summary="Create One",
73 | dependencies=create_route,
74 | )
75 |
76 | if delete_all_route:
77 | self._add_api_route(
78 | "",
79 | self._delete_all(),
80 | methods=["DELETE"],
81 | response_model=Optional[List[self.schema]], # type: ignore
82 | summary="Delete All",
83 | dependencies=delete_all_route,
84 | )
85 |
86 | if get_one_route:
87 | self._add_api_route(
88 | "/{item_id}",
89 | self._get_one(),
90 | methods=["GET"],
91 | response_model=self.schema,
92 | summary="Get One",
93 | dependencies=get_one_route,
94 | error_responses=[NOT_FOUND],
95 | )
96 |
97 | if update_route:
98 | self._add_api_route(
99 | "/{item_id}",
100 | self._update(),
101 | methods=["PUT"],
102 | response_model=self.schema,
103 | summary="Update One",
104 | dependencies=update_route,
105 | error_responses=[NOT_FOUND],
106 | )
107 |
108 | if delete_one_route:
109 | self._add_api_route(
110 | "/{item_id}",
111 | self._delete_one(),
112 | methods=["DELETE"],
113 | response_model=self.schema,
114 | summary="Delete One",
115 | dependencies=delete_one_route,
116 | error_responses=[NOT_FOUND],
117 | )
118 |
119 | def _add_api_route(
120 | self,
121 | path: str,
122 | endpoint: Callable[..., Any],
123 | dependencies: Union[bool, DEPENDENCIES],
124 | error_responses: Optional[List[HTTPException]] = None,
125 | **kwargs: Any,
126 | ) -> None:
127 | dependencies = [] if isinstance(dependencies, bool) else dependencies
128 | responses: Any = (
129 | {err.status_code: {"detail": err.detail} for err in error_responses}
130 | if error_responses
131 | else None
132 | )
133 |
134 | super().add_api_route(
135 | path, endpoint, dependencies=dependencies, responses=responses, **kwargs
136 | )
137 |
138 | def api_route(
139 | self, path: str, *args: Any, **kwargs: Any
140 | ) -> Callable[[DecoratedCallable], DecoratedCallable]:
141 | """Overrides and exiting route if it exists"""
142 | methods = kwargs["methods"] if "methods" in kwargs else ["GET"]
143 | self.remove_api_route(path, methods)
144 | return super().api_route(path, *args, **kwargs)
145 |
146 | def get(
147 | self, path: str, *args: Any, **kwargs: Any
148 | ) -> Callable[[DecoratedCallable], DecoratedCallable]:
149 | self.remove_api_route(path, ["Get"])
150 | return super().get(path, *args, **kwargs)
151 |
152 | def post(
153 | self, path: str, *args: Any, **kwargs: Any
154 | ) -> Callable[[DecoratedCallable], DecoratedCallable]:
155 | self.remove_api_route(path, ["POST"])
156 | return super().post(path, *args, **kwargs)
157 |
158 | def put(
159 | self, path: str, *args: Any, **kwargs: Any
160 | ) -> Callable[[DecoratedCallable], DecoratedCallable]:
161 | self.remove_api_route(path, ["PUT"])
162 | return super().put(path, *args, **kwargs)
163 |
164 | def delete(
165 | self, path: str, *args: Any, **kwargs: Any
166 | ) -> Callable[[DecoratedCallable], DecoratedCallable]:
167 | self.remove_api_route(path, ["DELETE"])
168 | return super().delete(path, *args, **kwargs)
169 |
170 | def remove_api_route(self, path: str, methods: List[str]) -> None:
171 | methods_ = set(methods)
172 |
173 | for route in self.routes:
174 | if (
175 | route.path == f"{self.prefix}{path}" # type: ignore
176 | and route.methods == methods_ # type: ignore
177 | ):
178 | self.routes.remove(route)
179 |
180 | @abstractmethod
181 | def _get_all(self, *args: Any, **kwargs: Any) -> Callable[..., Any]:
182 | raise NotImplementedError
183 |
184 | @abstractmethod
185 | def _get_one(self, *args: Any, **kwargs: Any) -> Callable[..., Any]:
186 | raise NotImplementedError
187 |
188 | @abstractmethod
189 | def _create(self, *args: Any, **kwargs: Any) -> Callable[..., Any]:
190 | raise NotImplementedError
191 |
192 | @abstractmethod
193 | def _update(self, *args: Any, **kwargs: Any) -> Callable[..., Any]:
194 | raise NotImplementedError
195 |
196 | @abstractmethod
197 | def _delete_one(self, *args: Any, **kwargs: Any) -> Callable[..., Any]:
198 | raise NotImplementedError
199 |
200 | @abstractmethod
201 | def _delete_all(self, *args: Any, **kwargs: Any) -> Callable[..., Any]:
202 | raise NotImplementedError
203 |
204 | def _raise(self, e: Exception, status_code: int = 422) -> HTTPException:
205 | raise HTTPException(422, ", ".join(e.args)) from e
206 |
207 | @staticmethod
208 | def get_routes() -> List[str]:
209 | return ["get_all", "create", "delete_all", "get_one", "update", "delete_one"]
210 |
--------------------------------------------------------------------------------
/fastapi_crudrouter/core/_types.py:
--------------------------------------------------------------------------------
1 | from typing import Dict, TypeVar, Optional, Sequence
2 |
3 | from fastapi.params import Depends
4 | from pydantic import BaseModel
5 |
6 | PAGINATION = Dict[str, Optional[int]]
7 | PYDANTIC_SCHEMA = BaseModel
8 |
9 | T = TypeVar("T", bound=BaseModel)
10 | DEPENDENCIES = Optional[Sequence[Depends]]
11 |
--------------------------------------------------------------------------------
/fastapi_crudrouter/core/_utils.py:
--------------------------------------------------------------------------------
1 | from typing import Optional, Type, Any
2 |
3 | from fastapi import Depends, HTTPException
4 | from pydantic import create_model
5 |
6 | from ._types import T, PAGINATION, PYDANTIC_SCHEMA
7 |
8 |
9 | class AttrDict(dict): # type: ignore
10 | def __init__(self, *args, **kwargs) -> None: # type: ignore
11 | super(AttrDict, self).__init__(*args, **kwargs)
12 | self.__dict__ = self
13 |
14 |
15 | def get_pk_type(schema: Type[PYDANTIC_SCHEMA], pk_field: str) -> Any:
16 | try:
17 | return schema.__fields__[pk_field].type_
18 | except KeyError:
19 | return int
20 |
21 |
22 | def schema_factory(
23 | schema_cls: Type[T], pk_field_name: str = "id", name: str = "Create"
24 | ) -> Type[T]:
25 | """
26 | Is used to create a CreateSchema which does not contain pk
27 | """
28 |
29 | fields = {
30 | f.name: (f.type_, ...)
31 | for f in schema_cls.__fields__.values()
32 | if f.name != pk_field_name
33 | }
34 |
35 | name = schema_cls.__name__ + name
36 | schema: Type[T] = create_model(__model_name=name, **fields) # type: ignore
37 | return schema
38 |
39 |
40 | def create_query_validation_exception(field: str, msg: str) -> HTTPException:
41 | return HTTPException(
42 | 422,
43 | detail={
44 | "detail": [
45 | {"loc": ["query", field], "msg": msg, "type": "type_error.integer"}
46 | ]
47 | },
48 | )
49 |
50 |
51 | def pagination_factory(max_limit: Optional[int] = None) -> Any:
52 | """
53 | Created the pagination dependency to be used in the router
54 | """
55 |
56 | def pagination(skip: int = 0, limit: Optional[int] = max_limit) -> PAGINATION:
57 | if skip < 0:
58 | raise create_query_validation_exception(
59 | field="skip",
60 | msg="skip query parameter must be greater or equal to zero",
61 | )
62 |
63 | if limit is not None:
64 | if limit <= 0:
65 | raise create_query_validation_exception(
66 | field="limit", msg="limit query parameter must be greater then zero"
67 | )
68 |
69 | elif max_limit and max_limit < limit:
70 | raise create_query_validation_exception(
71 | field="limit",
72 | msg=f"limit query parameter must be less then {max_limit}",
73 | )
74 |
75 | return {"skip": skip, "limit": limit}
76 |
77 | return Depends(pagination)
78 |
--------------------------------------------------------------------------------
/fastapi_crudrouter/core/databases.py:
--------------------------------------------------------------------------------
1 | from typing import (
2 | Any,
3 | Callable,
4 | List,
5 | Mapping,
6 | Type,
7 | Coroutine,
8 | Optional,
9 | Union,
10 | )
11 |
12 | from fastapi import HTTPException
13 |
14 | from . import CRUDGenerator, NOT_FOUND
15 | from ._types import PAGINATION, PYDANTIC_SCHEMA, DEPENDENCIES
16 | from ._utils import AttrDict, get_pk_type
17 |
18 | try:
19 | from sqlalchemy.sql.schema import Table
20 | from databases.core import Database
21 | except ImportError:
22 | Database = None # type: ignore
23 | Table = None
24 | databases_installed = False
25 | else:
26 | databases_installed = True
27 |
28 | Model = Mapping[Any, Any]
29 | CALLABLE = Callable[..., Coroutine[Any, Any, Model]]
30 | CALLABLE_LIST = Callable[..., Coroutine[Any, Any, List[Model]]]
31 |
32 |
33 | def pydantify_record(
34 | models: Union[Model, List[Model]]
35 | ) -> Union[AttrDict, List[AttrDict]]:
36 | if type(models) is list:
37 | return [AttrDict(**dict(model)) for model in models]
38 | else:
39 | return AttrDict(**dict(models)) # type: ignore
40 |
41 |
42 | class DatabasesCRUDRouter(CRUDGenerator[PYDANTIC_SCHEMA]):
43 | def __init__(
44 | self,
45 | schema: Type[PYDANTIC_SCHEMA],
46 | table: "Table",
47 | database: "Database",
48 | create_schema: Optional[Type[PYDANTIC_SCHEMA]] = None,
49 | update_schema: Optional[Type[PYDANTIC_SCHEMA]] = None,
50 | prefix: Optional[str] = None,
51 | tags: Optional[List[str]] = None,
52 | paginate: Optional[int] = None,
53 | get_all_route: Union[bool, DEPENDENCIES] = True,
54 | get_one_route: Union[bool, DEPENDENCIES] = True,
55 | create_route: Union[bool, DEPENDENCIES] = True,
56 | update_route: Union[bool, DEPENDENCIES] = True,
57 | delete_one_route: Union[bool, DEPENDENCIES] = True,
58 | delete_all_route: Union[bool, DEPENDENCIES] = True,
59 | **kwargs: Any
60 | ) -> None:
61 | assert (
62 | databases_installed
63 | ), "Databases and SQLAlchemy must be installed to use the DatabasesCRUDRouter."
64 |
65 | self.table = table
66 | self.db = database
67 | self._pk = table.primary_key.columns.values()[0].name
68 | self._pk_col = self.table.c[self._pk]
69 | self._pk_type: type = get_pk_type(schema, self._pk)
70 |
71 | super().__init__(
72 | schema=schema,
73 | create_schema=create_schema,
74 | update_schema=update_schema,
75 | prefix=prefix or table.name,
76 | tags=tags,
77 | paginate=paginate,
78 | get_all_route=get_all_route,
79 | get_one_route=get_one_route,
80 | create_route=create_route,
81 | update_route=update_route,
82 | delete_one_route=delete_one_route,
83 | delete_all_route=delete_all_route,
84 | **kwargs
85 | )
86 |
87 | def _get_all(self, *args: Any, **kwargs: Any) -> CALLABLE_LIST:
88 | async def route(
89 | pagination: PAGINATION = self.pagination,
90 | ) -> List[Model]:
91 | skip, limit = pagination.get("skip"), pagination.get("limit")
92 |
93 | query = self.table.select().limit(limit).offset(skip)
94 | return pydantify_record(await self.db.fetch_all(query)) # type: ignore
95 |
96 | return route
97 |
98 | def _get_one(self, *args: Any, **kwargs: Any) -> CALLABLE:
99 | async def route(item_id: self._pk_type) -> Model: # type: ignore
100 | query = self.table.select().where(self._pk_col == item_id)
101 | model = await self.db.fetch_one(query)
102 |
103 | if model:
104 | return pydantify_record(model) # type: ignore
105 | else:
106 | raise NOT_FOUND
107 |
108 | return route
109 |
110 | def _create(self, *args: Any, **kwargs: Any) -> CALLABLE:
111 | async def route(
112 | schema: self.create_schema, # type: ignore
113 | ) -> Model:
114 | query = self.table.insert()
115 |
116 | try:
117 | rid = await self.db.execute(query=query, values=schema.dict())
118 | if type(rid) is not self._pk_type:
119 | rid = getattr(schema, self._pk, rid)
120 |
121 | return await self._get_one()(rid)
122 | except Exception:
123 | raise HTTPException(422, "Key already exists") from None
124 |
125 | return route
126 |
127 | def _update(self, *args: Any, **kwargs: Any) -> CALLABLE:
128 | async def route(
129 | item_id: self._pk_type, schema: self.update_schema # type: ignore
130 | ) -> Model:
131 | query = self.table.update().where(self._pk_col == item_id)
132 |
133 | try:
134 | await self.db.fetch_one(
135 | query=query, values=schema.dict(exclude={self._pk})
136 | )
137 | return await self._get_one()(item_id)
138 | except Exception as e:
139 | raise NOT_FOUND from e
140 |
141 | return route
142 |
143 | def _delete_all(self, *args: Any, **kwargs: Any) -> CALLABLE_LIST:
144 | async def route() -> List[Model]:
145 | query = self.table.delete()
146 | await self.db.execute(query=query)
147 |
148 | return await self._get_all()(pagination={"skip": 0, "limit": None})
149 |
150 | return route
151 |
152 | def _delete_one(self, *args: Any, **kwargs: Any) -> CALLABLE:
153 | async def route(item_id: self._pk_type) -> Model: # type: ignore
154 | query = self.table.delete().where(self._pk_col == item_id)
155 |
156 | try:
157 | row = await self._get_one()(item_id)
158 | await self.db.execute(query=query)
159 | return row
160 | except Exception as e:
161 | raise NOT_FOUND from e
162 |
163 | return route
164 |
--------------------------------------------------------------------------------
/fastapi_crudrouter/core/gino_starlette.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Callable, List, Optional, Type, Union, Coroutine
2 |
3 | from fastapi import HTTPException
4 |
5 | from . import NOT_FOUND, CRUDGenerator, _utils
6 | from ._types import DEPENDENCIES, PAGINATION
7 | from ._types import PYDANTIC_SCHEMA as SCHEMA
8 |
9 | try:
10 | from asyncpg.exceptions import UniqueViolationError
11 | from gino import Gino
12 | from sqlalchemy.exc import IntegrityError
13 | from sqlalchemy.ext.declarative import DeclarativeMeta as Model
14 | except ImportError:
15 | Model = None
16 | IntegrityError = None
17 | UniqueViolationError = None
18 | Gino = None
19 | gino_installed = False
20 | else:
21 | gino_installed = True
22 |
23 | CALLABLE = Callable[..., Coroutine[Any, Any, Model]]
24 | CALLABLE_LIST = Callable[..., Coroutine[Any, Any, List[Model]]]
25 |
26 |
27 | class GinoCRUDRouter(CRUDGenerator[SCHEMA]):
28 | def __init__(
29 | self,
30 | schema: Type[SCHEMA],
31 | db_model: Model,
32 | db: "Gino",
33 | create_schema: Optional[Type[SCHEMA]] = None,
34 | update_schema: Optional[Type[SCHEMA]] = None,
35 | prefix: Optional[str] = None,
36 | tags: Optional[List[str]] = None,
37 | paginate: Optional[int] = None,
38 | get_all_route: Union[bool, DEPENDENCIES] = True,
39 | get_one_route: Union[bool, DEPENDENCIES] = True,
40 | create_route: Union[bool, DEPENDENCIES] = True,
41 | update_route: Union[bool, DEPENDENCIES] = True,
42 | delete_one_route: Union[bool, DEPENDENCIES] = True,
43 | delete_all_route: Union[bool, DEPENDENCIES] = True,
44 | **kwargs: Any
45 | ) -> None:
46 | assert gino_installed, "Gino must be installed to use the GinoCRUDRouter."
47 |
48 | self.db_model = db_model
49 | self.db = db
50 | self._pk: str = db_model.__table__.primary_key.columns.keys()[0]
51 | self._pk_type: type = _utils.get_pk_type(schema, self._pk)
52 |
53 | super().__init__(
54 | schema=schema,
55 | create_schema=create_schema,
56 | update_schema=update_schema,
57 | prefix=prefix or db_model.__tablename__,
58 | tags=tags,
59 | paginate=paginate,
60 | get_all_route=get_all_route,
61 | get_one_route=get_one_route,
62 | create_route=create_route,
63 | update_route=update_route,
64 | delete_one_route=delete_one_route,
65 | delete_all_route=delete_all_route,
66 | **kwargs
67 | )
68 |
69 | def _get_all(self, *args: Any, **kwargs: Any) -> CALLABLE_LIST:
70 | async def route(
71 | pagination: PAGINATION = self.pagination,
72 | ) -> List[Model]:
73 | skip, limit = pagination.get("skip"), pagination.get("limit")
74 |
75 | db_models: List[Model] = (
76 | await self.db_model.query.limit(limit).offset(skip).gino.all()
77 | )
78 | return db_models
79 |
80 | return route
81 |
82 | def _get_one(self, *args: Any, **kwargs: Any) -> CALLABLE:
83 | async def route(item_id: self._pk_type) -> Model: # type: ignore
84 | model: Model = await self.db_model.get(item_id)
85 |
86 | if model:
87 | return model
88 | else:
89 | raise NOT_FOUND
90 |
91 | return route
92 |
93 | def _create(self, *args: Any, **kwargs: Any) -> CALLABLE:
94 | async def route(
95 | model: self.create_schema, # type: ignore
96 | ) -> Model:
97 | try:
98 | async with self.db.transaction():
99 | db_model: Model = await self.db_model.create(**model.dict())
100 | return db_model
101 | except (IntegrityError, UniqueViolationError):
102 | raise HTTPException(422, "Key already exists") from None
103 |
104 | return route
105 |
106 | def _update(self, *args: Any, **kwargs: Any) -> CALLABLE:
107 | async def route(
108 | item_id: self._pk_type, # type: ignore
109 | model: self.update_schema, # type: ignore
110 | ) -> Model:
111 | try:
112 | db_model: Model = await self._get_one()(item_id)
113 | async with self.db.transaction():
114 | model = model.dict(exclude={self._pk})
115 | await db_model.update(**model).apply()
116 |
117 | return db_model
118 | except (IntegrityError, UniqueViolationError) as e:
119 | self._raise(e)
120 |
121 | return route
122 |
123 | def _delete_all(self, *args: Any, **kwargs: Any) -> CALLABLE_LIST:
124 | async def route() -> List[Model]:
125 | await self.db_model.delete.gino.status()
126 | return await self._get_all()(pagination={"skip": 0, "limit": None})
127 |
128 | return route
129 |
130 | def _delete_one(self, *args: Any, **kwargs: Any) -> CALLABLE:
131 | async def route(item_id: self._pk_type) -> Model: # type: ignore
132 | db_model: Model = await self._get_one()(item_id)
133 | await db_model.delete()
134 |
135 | return db_model
136 |
137 | return route
138 |
--------------------------------------------------------------------------------
/fastapi_crudrouter/core/mem.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Callable, List, Type, cast, Optional, Union
2 |
3 | from . import CRUDGenerator, NOT_FOUND
4 | from ._types import DEPENDENCIES, PAGINATION, PYDANTIC_SCHEMA as SCHEMA
5 |
6 | CALLABLE = Callable[..., SCHEMA]
7 | CALLABLE_LIST = Callable[..., List[SCHEMA]]
8 |
9 |
10 | class MemoryCRUDRouter(CRUDGenerator[SCHEMA]):
11 | def __init__(
12 | self,
13 | schema: Type[SCHEMA],
14 | create_schema: Optional[Type[SCHEMA]] = None,
15 | update_schema: Optional[Type[SCHEMA]] = None,
16 | prefix: Optional[str] = None,
17 | tags: Optional[List[str]] = None,
18 | paginate: Optional[int] = None,
19 | get_all_route: Union[bool, DEPENDENCIES] = True,
20 | get_one_route: Union[bool, DEPENDENCIES] = True,
21 | create_route: Union[bool, DEPENDENCIES] = True,
22 | update_route: Union[bool, DEPENDENCIES] = True,
23 | delete_one_route: Union[bool, DEPENDENCIES] = True,
24 | delete_all_route: Union[bool, DEPENDENCIES] = True,
25 | **kwargs: Any
26 | ) -> None:
27 | super().__init__(
28 | schema=schema,
29 | create_schema=create_schema,
30 | update_schema=update_schema,
31 | prefix=prefix,
32 | tags=tags,
33 | paginate=paginate,
34 | get_all_route=get_all_route,
35 | get_one_route=get_one_route,
36 | create_route=create_route,
37 | update_route=update_route,
38 | delete_one_route=delete_one_route,
39 | delete_all_route=delete_all_route,
40 | **kwargs
41 | )
42 |
43 | self.models: List[SCHEMA] = []
44 | self._id = 1
45 |
46 | def _get_all(self, *args: Any, **kwargs: Any) -> CALLABLE_LIST:
47 | def route(pagination: PAGINATION = self.pagination) -> List[SCHEMA]:
48 | skip, limit = pagination.get("skip"), pagination.get("limit")
49 | skip = cast(int, skip)
50 |
51 | return (
52 | self.models[skip:]
53 | if limit is None
54 | else self.models[skip : skip + limit]
55 | )
56 |
57 | return route
58 |
59 | def _get_one(self, *args: Any, **kwargs: Any) -> CALLABLE:
60 | def route(item_id: int) -> SCHEMA:
61 | for model in self.models:
62 | if model.id == item_id: # type: ignore
63 | return model
64 |
65 | raise NOT_FOUND
66 |
67 | return route
68 |
69 | def _create(self, *args: Any, **kwargs: Any) -> CALLABLE:
70 | def route(model: self.create_schema) -> SCHEMA: # type: ignore
71 | model_dict = model.dict()
72 | model_dict["id"] = self._get_next_id()
73 | ready_model = self.schema(**model_dict)
74 | self.models.append(ready_model)
75 | return ready_model
76 |
77 | return route
78 |
79 | def _update(self, *args: Any, **kwargs: Any) -> CALLABLE:
80 | def route(item_id: int, model: self.update_schema) -> SCHEMA: # type: ignore
81 | for ind, model_ in enumerate(self.models):
82 | if model_.id == item_id: # type: ignore
83 | self.models[ind] = self.schema(
84 | **model.dict(), id=model_.id # type: ignore
85 | )
86 | return self.models[ind]
87 |
88 | raise NOT_FOUND
89 |
90 | return route
91 |
92 | def _delete_all(self, *args: Any, **kwargs: Any) -> CALLABLE_LIST:
93 | def route() -> List[SCHEMA]:
94 | self.models = []
95 | return self.models
96 |
97 | return route
98 |
99 | def _delete_one(self, *args: Any, **kwargs: Any) -> CALLABLE:
100 | def route(item_id: int) -> SCHEMA:
101 | for ind, model in enumerate(self.models):
102 | if model.id == item_id: # type: ignore
103 | del self.models[ind]
104 | return model
105 |
106 | raise NOT_FOUND
107 |
108 | return route
109 |
110 | def _get_next_id(self) -> int:
111 | id_ = self._id
112 | self._id += 1
113 |
114 | return id_
115 |
--------------------------------------------------------------------------------
/fastapi_crudrouter/core/ormar.py:
--------------------------------------------------------------------------------
1 | from typing import (
2 | Any,
3 | Callable,
4 | List,
5 | Optional,
6 | Type,
7 | cast,
8 | Coroutine,
9 | Union,
10 | )
11 |
12 | from fastapi import HTTPException
13 |
14 | from . import CRUDGenerator, NOT_FOUND, _utils
15 | from ._types import DEPENDENCIES, PAGINATION
16 |
17 | try:
18 | from ormar import Model, NoMatch
19 | except ImportError:
20 | Model = None # type: ignore
21 | NoMatch = None # type: ignore
22 | ormar_installed = False
23 | else:
24 | ormar_installed = True
25 |
26 | CALLABLE = Callable[..., Coroutine[Any, Any, Model]]
27 | CALLABLE_LIST = Callable[..., Coroutine[Any, Any, List[Optional[Model]]]]
28 |
29 |
30 | class OrmarCRUDRouter(CRUDGenerator[Model]):
31 | def __init__(
32 | self,
33 | schema: Type[Model],
34 | create_schema: Optional[Type[Model]] = None,
35 | update_schema: Optional[Type[Model]] = None,
36 | prefix: Optional[str] = None,
37 | tags: Optional[List[str]] = None,
38 | paginate: Optional[int] = None,
39 | get_all_route: Union[bool, DEPENDENCIES] = True,
40 | get_one_route: Union[bool, DEPENDENCIES] = True,
41 | create_route: Union[bool, DEPENDENCIES] = True,
42 | update_route: Union[bool, DEPENDENCIES] = True,
43 | delete_one_route: Union[bool, DEPENDENCIES] = True,
44 | delete_all_route: Union[bool, DEPENDENCIES] = True,
45 | **kwargs: Any
46 | ) -> None:
47 | assert ormar_installed, "Ormar must be installed to use the OrmarCRUDRouter."
48 |
49 | self._pk: str = schema.Meta.pkname
50 | self._pk_type: type = _utils.get_pk_type(schema, self._pk)
51 |
52 | super().__init__(
53 | schema=schema,
54 | create_schema=create_schema or schema,
55 | update_schema=update_schema or schema,
56 | prefix=prefix or schema.Meta.tablename,
57 | tags=tags,
58 | paginate=paginate,
59 | get_all_route=get_all_route,
60 | get_one_route=get_one_route,
61 | create_route=create_route,
62 | update_route=update_route,
63 | delete_one_route=delete_one_route,
64 | delete_all_route=delete_all_route,
65 | **kwargs
66 | )
67 |
68 | self._INTEGRITY_ERROR = self._get_integrity_error_type()
69 |
70 | def _get_all(self, *args: Any, **kwargs: Any) -> CALLABLE_LIST:
71 | async def route(
72 | pagination: PAGINATION = self.pagination,
73 | ) -> List[Optional[Model]]:
74 | skip, limit = pagination.get("skip"), pagination.get("limit")
75 | query = self.schema.objects.offset(cast(int, skip))
76 | if limit:
77 | query = query.limit(limit)
78 | return await query.all() # type: ignore
79 |
80 | return route
81 |
82 | def _get_one(self, *args: Any, **kwargs: Any) -> CALLABLE:
83 | async def route(item_id: self._pk_type) -> Model: # type: ignore
84 | try:
85 | filter_ = {self._pk: item_id}
86 | model = await self.schema.objects.filter(
87 | _exclude=False, **filter_
88 | ).first()
89 | except NoMatch:
90 | raise NOT_FOUND from None
91 | return model
92 |
93 | return route
94 |
95 | def _create(self, *args: Any, **kwargs: Any) -> CALLABLE:
96 | async def route(model: self.create_schema) -> Model: # type: ignore
97 | model_dict = model.dict()
98 | if self.schema.Meta.model_fields[self._pk].autoincrement:
99 | model_dict.pop(self._pk, None)
100 | try:
101 | return await self.schema.objects.create(**model_dict)
102 | except self._INTEGRITY_ERROR:
103 | raise HTTPException(422, "Key already exists") from None
104 |
105 | return route
106 |
107 | def _update(self, *args: Any, **kwargs: Any) -> CALLABLE:
108 | async def route(
109 | item_id: self._pk_type, # type: ignore
110 | model: self.update_schema, # type: ignore
111 | ) -> Model:
112 | filter_ = {self._pk: item_id}
113 | try:
114 | await self.schema.objects.filter(_exclude=False, **filter_).update(
115 | **model.dict(exclude_unset=True)
116 | )
117 | except self._INTEGRITY_ERROR as e:
118 | self._raise(e)
119 | return await self._get_one()(item_id)
120 |
121 | return route
122 |
123 | def _delete_all(self, *args: Any, **kwargs: Any) -> CALLABLE_LIST:
124 | async def route() -> List[Optional[Model]]:
125 | await self.schema.objects.delete(each=True)
126 | return await self._get_all()(pagination={"skip": 0, "limit": None})
127 |
128 | return route
129 |
130 | def _delete_one(self, *args: Any, **kwargs: Any) -> CALLABLE:
131 | async def route(item_id: self._pk_type) -> Model: # type: ignore
132 | model = await self._get_one()(item_id)
133 | await model.delete()
134 | return model
135 |
136 | return route
137 |
138 | def _get_integrity_error_type(self) -> Type[Exception]:
139 | """Imports the Integrity exception based on the used backend"""
140 | backend = self.schema.db_backend_name()
141 |
142 | try:
143 | if backend == "sqlite":
144 | from sqlite3 import IntegrityError
145 | elif backend == "postgresql":
146 | from asyncpg import ( # type: ignore
147 | IntegrityConstraintViolationError as IntegrityError,
148 | )
149 | else:
150 | from pymysql import IntegrityError # type: ignore
151 | return IntegrityError
152 | except ImportError:
153 | return Exception
154 |
--------------------------------------------------------------------------------
/fastapi_crudrouter/core/sqlalchemy.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Callable, List, Type, Generator, Optional, Union
2 |
3 | from fastapi import Depends, HTTPException
4 |
5 | from . import CRUDGenerator, NOT_FOUND, _utils
6 | from ._types import DEPENDENCIES, PAGINATION, PYDANTIC_SCHEMA as SCHEMA
7 |
8 | try:
9 | from sqlalchemy.orm import Session
10 | from sqlalchemy.ext.declarative import DeclarativeMeta as Model
11 | from sqlalchemy.exc import IntegrityError
12 | except ImportError:
13 | Model = None
14 | Session = None
15 | IntegrityError = None
16 | sqlalchemy_installed = False
17 | else:
18 | sqlalchemy_installed = True
19 | Session = Callable[..., Generator[Session, Any, None]]
20 |
21 | CALLABLE = Callable[..., Model]
22 | CALLABLE_LIST = Callable[..., List[Model]]
23 |
24 |
25 | class SQLAlchemyCRUDRouter(CRUDGenerator[SCHEMA]):
26 | def __init__(
27 | self,
28 | schema: Type[SCHEMA],
29 | db_model: Model,
30 | db: "Session",
31 | create_schema: Optional[Type[SCHEMA]] = None,
32 | update_schema: Optional[Type[SCHEMA]] = None,
33 | prefix: Optional[str] = None,
34 | tags: Optional[List[str]] = None,
35 | paginate: Optional[int] = None,
36 | get_all_route: Union[bool, DEPENDENCIES] = True,
37 | get_one_route: Union[bool, DEPENDENCIES] = True,
38 | create_route: Union[bool, DEPENDENCIES] = True,
39 | update_route: Union[bool, DEPENDENCIES] = True,
40 | delete_one_route: Union[bool, DEPENDENCIES] = True,
41 | delete_all_route: Union[bool, DEPENDENCIES] = True,
42 | **kwargs: Any
43 | ) -> None:
44 | assert (
45 | sqlalchemy_installed
46 | ), "SQLAlchemy must be installed to use the SQLAlchemyCRUDRouter."
47 |
48 | self.db_model = db_model
49 | self.db_func = db
50 | self._pk: str = db_model.__table__.primary_key.columns.keys()[0]
51 | self._pk_type: type = _utils.get_pk_type(schema, self._pk)
52 |
53 | super().__init__(
54 | schema=schema,
55 | create_schema=create_schema,
56 | update_schema=update_schema,
57 | prefix=prefix or db_model.__tablename__,
58 | tags=tags,
59 | paginate=paginate,
60 | get_all_route=get_all_route,
61 | get_one_route=get_one_route,
62 | create_route=create_route,
63 | update_route=update_route,
64 | delete_one_route=delete_one_route,
65 | delete_all_route=delete_all_route,
66 | **kwargs
67 | )
68 |
69 | def _get_all(self, *args: Any, **kwargs: Any) -> CALLABLE_LIST:
70 | def route(
71 | db: Session = Depends(self.db_func),
72 | pagination: PAGINATION = self.pagination,
73 | ) -> List[Model]:
74 | skip, limit = pagination.get("skip"), pagination.get("limit")
75 |
76 | db_models: List[Model] = (
77 | db.query(self.db_model)
78 | .order_by(getattr(self.db_model, self._pk))
79 | .limit(limit)
80 | .offset(skip)
81 | .all()
82 | )
83 | return db_models
84 |
85 | return route
86 |
87 | def _get_one(self, *args: Any, **kwargs: Any) -> CALLABLE:
88 | def route(
89 | item_id: self._pk_type, db: Session = Depends(self.db_func) # type: ignore
90 | ) -> Model:
91 | model: Model = db.query(self.db_model).get(item_id)
92 |
93 | if model:
94 | return model
95 | else:
96 | raise NOT_FOUND from None
97 |
98 | return route
99 |
100 | def _create(self, *args: Any, **kwargs: Any) -> CALLABLE:
101 | def route(
102 | model: self.create_schema, # type: ignore
103 | db: Session = Depends(self.db_func),
104 | ) -> Model:
105 | try:
106 | db_model: Model = self.db_model(**model.dict())
107 | db.add(db_model)
108 | db.commit()
109 | db.refresh(db_model)
110 | return db_model
111 | except IntegrityError:
112 | db.rollback()
113 | raise HTTPException(422, "Key already exists") from None
114 |
115 | return route
116 |
117 | def _update(self, *args: Any, **kwargs: Any) -> CALLABLE:
118 | def route(
119 | item_id: self._pk_type, # type: ignore
120 | model: self.update_schema, # type: ignore
121 | db: Session = Depends(self.db_func),
122 | ) -> Model:
123 | try:
124 | db_model: Model = self._get_one()(item_id, db)
125 |
126 | for key, value in model.dict(exclude={self._pk}).items():
127 | if hasattr(db_model, key):
128 | setattr(db_model, key, value)
129 |
130 | db.commit()
131 | db.refresh(db_model)
132 |
133 | return db_model
134 | except IntegrityError as e:
135 | db.rollback()
136 | self._raise(e)
137 |
138 | return route
139 |
140 | def _delete_all(self, *args: Any, **kwargs: Any) -> CALLABLE_LIST:
141 | def route(db: Session = Depends(self.db_func)) -> List[Model]:
142 | db.query(self.db_model).delete()
143 | db.commit()
144 |
145 | return self._get_all()(db=db, pagination={"skip": 0, "limit": None})
146 |
147 | return route
148 |
149 | def _delete_one(self, *args: Any, **kwargs: Any) -> CALLABLE:
150 | def route(
151 | item_id: self._pk_type, db: Session = Depends(self.db_func) # type: ignore
152 | ) -> Model:
153 | db_model: Model = self._get_one()(item_id, db)
154 | db.delete(db_model)
155 | db.commit()
156 |
157 | return db_model
158 |
159 | return route
160 |
--------------------------------------------------------------------------------
/fastapi_crudrouter/core/tortoise.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Callable, List, Type, cast, Coroutine, Optional, Union
2 |
3 | from . import CRUDGenerator, NOT_FOUND
4 | from ._types import DEPENDENCIES, PAGINATION, PYDANTIC_SCHEMA as SCHEMA
5 |
6 | try:
7 | from tortoise.models import Model
8 | except ImportError:
9 | Model = None # type: ignore
10 | tortoise_installed = False
11 | else:
12 | tortoise_installed = True
13 |
14 |
15 | CALLABLE = Callable[..., Coroutine[Any, Any, Model]]
16 | CALLABLE_LIST = Callable[..., Coroutine[Any, Any, List[Model]]]
17 |
18 |
19 | class TortoiseCRUDRouter(CRUDGenerator[SCHEMA]):
20 | def __init__(
21 | self,
22 | schema: Type[SCHEMA],
23 | db_model: Type[Model],
24 | create_schema: Optional[Type[SCHEMA]] = None,
25 | update_schema: Optional[Type[SCHEMA]] = None,
26 | prefix: Optional[str] = None,
27 | tags: Optional[List[str]] = None,
28 | paginate: Optional[int] = None,
29 | get_all_route: Union[bool, DEPENDENCIES] = True,
30 | get_one_route: Union[bool, DEPENDENCIES] = True,
31 | create_route: Union[bool, DEPENDENCIES] = True,
32 | update_route: Union[bool, DEPENDENCIES] = True,
33 | delete_one_route: Union[bool, DEPENDENCIES] = True,
34 | delete_all_route: Union[bool, DEPENDENCIES] = True,
35 | **kwargs: Any
36 | ) -> None:
37 | assert (
38 | tortoise_installed
39 | ), "Tortoise ORM must be installed to use the TortoiseCRUDRouter."
40 |
41 | self.db_model = db_model
42 | self._pk: str = db_model.describe()["pk_field"]["db_column"]
43 |
44 | super().__init__(
45 | schema=schema,
46 | create_schema=create_schema,
47 | update_schema=update_schema,
48 | prefix=prefix or db_model.describe()["name"].replace("None.", ""),
49 | tags=tags,
50 | paginate=paginate,
51 | get_all_route=get_all_route,
52 | get_one_route=get_one_route,
53 | create_route=create_route,
54 | update_route=update_route,
55 | delete_one_route=delete_one_route,
56 | delete_all_route=delete_all_route,
57 | **kwargs
58 | )
59 |
60 | def _get_all(self, *args: Any, **kwargs: Any) -> CALLABLE_LIST:
61 | async def route(pagination: PAGINATION = self.pagination) -> List[Model]:
62 | skip, limit = pagination.get("skip"), pagination.get("limit")
63 | query = self.db_model.all().offset(cast(int, skip))
64 | if limit:
65 | query = query.limit(limit)
66 | return await query
67 |
68 | return route
69 |
70 | def _get_one(self, *args: Any, **kwargs: Any) -> CALLABLE:
71 | async def route(item_id: int) -> Model:
72 | model = await self.db_model.filter(id=item_id).first()
73 |
74 | if model:
75 | return model
76 | else:
77 | raise NOT_FOUND
78 |
79 | return route
80 |
81 | def _create(self, *args: Any, **kwargs: Any) -> CALLABLE:
82 | async def route(model: self.create_schema) -> Model: # type: ignore
83 | db_model = self.db_model(**model.dict())
84 | await db_model.save()
85 |
86 | return db_model
87 |
88 | return route
89 |
90 | def _update(self, *args: Any, **kwargs: Any) -> CALLABLE:
91 | async def route(
92 | item_id: int, model: self.update_schema # type: ignore
93 | ) -> Model:
94 | await self.db_model.filter(id=item_id).update(
95 | **model.dict(exclude_unset=True)
96 | )
97 | return await self._get_one()(item_id)
98 |
99 | return route
100 |
101 | def _delete_all(self, *args: Any, **kwargs: Any) -> CALLABLE_LIST:
102 | async def route() -> List[Model]:
103 | await self.db_model.all().delete()
104 | return await self._get_all()(pagination={"skip": 0, "limit": None})
105 |
106 | return route
107 |
108 | def _delete_one(self, *args: Any, **kwargs: Any) -> CALLABLE:
109 | async def route(item_id: int) -> Model:
110 | model: Model = await self._get_one()(item_id)
111 | await self.db_model.filter(id=item_id).delete()
112 |
113 | return model
114 |
115 | return route
116 |
--------------------------------------------------------------------------------
/fastapi_crudrouter/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awtkns/fastapi-crudrouter/9b829865d85113a3f16f94c029502a9a584d47bb/fastapi_crudrouter/py.typed
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | fastapi>=0.62.0
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [flake8]
2 | max-line-length = 88
3 | select = C,E,F,W,B,B9
4 | ignore = B008, E203, W503, CFQ001, CFQ002, ECE001
5 | import-order-style = pycharm
6 |
7 | [mypy]
8 | disallow_any_generics = True
9 | disallow_subclassing_any = True
10 | disallow_untyped_calls = True
11 | disallow_untyped_defs = True
12 | disallow_incomplete_defs = True
13 | check_untyped_defs = True
14 | disallow_untyped_decorators = True
15 | no_implicit_optional = True
16 | warn_redundant_casts = True
17 | warn_unused_ignores = True
18 | warn_return_any = True
19 | implicit_reexport = False
20 | strict_equality = True
21 |
22 | [mypy-tests.*]
23 | ignore_errors = True
24 |
25 | [mypy-venv.*]
26 | ignore_errors = True
27 |
28 | [mypy-sqlalchemy.*]
29 | ignore_missing_imports = True
30 |
31 | [mypy-sqlalchemy_utils.*]
32 | ignore_missing_imports = True
33 |
34 | [mypy-uvicorn.*]
35 | ignore_missing_imports = True
36 |
37 | [mypy-gino.*]
38 | ignore_missing_imports = True
39 |
40 | [mypy-asyncpg.*]
41 | ignore_missing_imports = True
42 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 | from distutils.util import convert_path
3 |
4 |
5 | def get_version():
6 | ver_path = convert_path("fastapi_crudrouter/_version.py")
7 | with open(ver_path) as ver_file:
8 | main_ns = {}
9 | exec(ver_file.read(), main_ns)
10 | return main_ns["__version__"]
11 |
12 |
13 | setup(
14 | name="fastapi-crudrouter",
15 | version=get_version(),
16 | author="Adam Watkins",
17 | author_email="hello@awtkns.com",
18 | packages=find_packages(exclude=("tests.*", "tests")),
19 | url="https://github.com/awtkns/fastapi-crudrouter",
20 | documentation="https://fastapi-crudrouter.awtkns.com/",
21 | license="MIT",
22 | description="A dynamic FastAPI router that automatically creates CRUD routes for your models",
23 | long_description=open("README.md").read(),
24 | long_description_content_type="text/markdown",
25 | install_requires=["fastapi"],
26 | python_requires=">=3.7",
27 | keywords=["fastapi", "crud", "restful", "routing", "generator", "crudrouter"],
28 | classifiers=[
29 | "Operating System :: OS Independent",
30 | "Programming Language :: Python :: 3",
31 | "Programming Language :: Python",
32 | "Topic :: Internet",
33 | "Topic :: Software Development :: Libraries :: Application Frameworks",
34 | "Topic :: Software Development :: Libraries :: Python Modules",
35 | "Topic :: Software Development :: Libraries",
36 | "Topic :: Software Development :: Code Generators",
37 | "Topic :: Software Development",
38 | "Typing :: Typed",
39 | "Development Status :: 4 - Beta",
40 | "Environment :: Web Environment",
41 | "Framework :: AsyncIO",
42 | "Intended Audience :: Developers",
43 | "Intended Audience :: Information Technology",
44 | "Intended Audience :: System Administrators",
45 | "License :: OSI Approved :: MIT License",
46 | "Programming Language :: Python :: 3 :: Only",
47 | "Programming Language :: Python :: 3.7",
48 | "Programming Language :: Python :: 3.8",
49 | "Programming Language :: Python :: 3.9",
50 | "Programming Language :: Python :: 3.10",
51 | "Programming Language :: Python :: 3.11",
52 | "Topic :: Internet :: WWW/HTTP :: HTTP Servers",
53 | "Topic :: Internet :: WWW/HTTP",
54 | ],
55 | )
56 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel
2 |
3 | from .conf import config
4 |
5 | PAGINATION_SIZE = 10
6 | CUSTOM_TAGS = ["Tag1", "Tag2"]
7 |
8 |
9 | class ORMModel(BaseModel):
10 | id: int
11 |
12 | class Config:
13 | orm_mode = True
14 |
15 |
16 | class PotatoCreate(BaseModel):
17 | thickness: float
18 | mass: float
19 | color: str
20 | type: str
21 |
22 |
23 | class Potato(PotatoCreate, ORMModel):
24 | pass
25 |
26 |
27 | class CustomPotato(PotatoCreate):
28 | potato_id: int
29 |
30 | class Config:
31 | orm_mode = True
32 |
33 |
34 | class CarrotCreate(BaseModel):
35 | length: float
36 | color: str = "Orange"
37 |
38 |
39 | class CarrotUpdate(BaseModel):
40 | length: float
41 |
42 |
43 | class Carrot(CarrotCreate, ORMModel):
44 | pass
45 |
46 |
47 | class PotatoType(BaseModel):
48 | name: str
49 | origin: str
50 |
51 | class Config:
52 | orm_mode = True
53 |
--------------------------------------------------------------------------------
/tests/conf/__init__.py:
--------------------------------------------------------------------------------
1 | from .config import BaseConfig
2 |
3 | config = BaseConfig()
4 |
--------------------------------------------------------------------------------
/tests/conf/config.py:
--------------------------------------------------------------------------------
1 | import os
2 | import pathlib
3 |
4 |
5 | ENV_FILE_PATH = pathlib.Path(__file__).parent / "dev.env"
6 | assert ENV_FILE_PATH.exists()
7 |
8 |
9 | class BaseConfig:
10 | POSTGRES_HOST = ""
11 | POSTGRES_USER = ""
12 | POSTGRES_PASSWORD = ""
13 | POSTGRES_DB = ""
14 | POSTGRES_PORT = ""
15 |
16 | MSSQL_PORT = ""
17 | SA_PASSWORD = ""
18 |
19 | def __init__(self):
20 | self._apply_dot_env()
21 | self._apply_env_vars()
22 | self.POSTGRES_URI = f"postgresql://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}"
23 | self.MSSQL_URI = f"mssql+pyodbc://sa:{self.SA_PASSWORD}@{self.POSTGRES_HOST}:{self.MSSQL_PORT}/test?driver=SQL+Server"
24 |
25 | def _apply_dot_env(self):
26 | with open(ENV_FILE_PATH) as fp:
27 | for line in fp.readlines():
28 | line = line.strip(" \n")
29 |
30 | if line and not line.startswith("#"):
31 | k, v = line.split("=", 1)
32 |
33 | if hasattr(self, k) and not getattr(self, k):
34 | setattr(self, k, v)
35 |
36 | def _apply_env_vars(self):
37 | for k, v in os.environ.items():
38 | if hasattr(self, k):
39 | setattr(self, k, v)
40 |
--------------------------------------------------------------------------------
/tests/conf/dev.docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.9"
2 |
3 | services:
4 | db:
5 | image: postgres
6 | restart: always
7 | env_file:
8 | - dev.env
9 | ports:
10 | - 5432:5432
11 | # mssql:
12 | # image: mcr.microsoft.com/mssql/server:2019-latest
13 | # command: /bin/sh -c "(/opt/mssql/bin/sqlservr &) && sleep 10s && /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P Password1! -Q 'CREATE DATABASE test' && sleep infinity"
14 | # restart: always
15 | # env_file:
16 | # - dev.env
17 | # ports:
18 | # - 1433:1433
--------------------------------------------------------------------------------
/tests/conf/dev.env:
--------------------------------------------------------------------------------
1 | POSTGRES_HOST=localhost
2 | POSTGRES_DB=test
3 | POSTGRES_USER=postgres
4 | POSTGRES_PASSWORD=password
5 | POSTGRES_PORT=5432
6 |
7 | ACCEPT_EULA=Y
8 | SA_PASSWORD=Password1!
9 | MSSQL_PORT=1433
10 | MSSQL_PID=Express
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from fastapi.testclient import TestClient
3 |
4 | from .implementations import *
5 |
6 |
7 | def yield_test_client(app, impl):
8 | if impl.__name__ == "tortoise_implementation":
9 | from tortoise.contrib.test import initializer, finalizer
10 |
11 | initializer(["tests.implementations.tortoise_"])
12 | with TestClient(app) as c:
13 | yield c
14 | finalizer()
15 |
16 | else:
17 | with TestClient(app) as c:
18 | yield c
19 |
20 |
21 | def label_func(*args):
22 | func, dsn = args[0]
23 | dsn = dsn.split(":")[0].split("+")[0]
24 | return f"{func.__name__}-{dsn}"
25 |
26 |
27 | @pytest.fixture(params=implementations, ids=label_func, scope="class")
28 | def client(request):
29 | impl, dsn = request.param
30 |
31 | app, router, settings = impl(db_uri=dsn)
32 | [app.include_router(router(**kwargs)) for kwargs in settings]
33 | yield from yield_test_client(app, impl)
34 |
35 |
36 | @pytest.fixture(
37 | params=[
38 | sqlalchemy_implementation_custom_ids,
39 | databases_implementation_custom_ids,
40 | ormar_implementation_custom_ids,
41 | gino_implementation_custom_ids,
42 | ]
43 | )
44 | def custom_id_client(request):
45 | yield from yield_test_client(request.param(), request.param)
46 |
47 |
48 | @pytest.fixture(
49 | params=[
50 | sqlalchemy_implementation_string_pk,
51 | databases_implementation_string_pk,
52 | ormar_implementation_string_pk,
53 | gino_implementation_string_pk,
54 | ],
55 | scope="function",
56 | )
57 | def string_pk_client(request):
58 | yield from yield_test_client(request.param(), request.param)
59 |
60 |
61 | @pytest.fixture(
62 | params=[
63 | sqlalchemy_implementation_integrity_errors,
64 | ormar_implementation_integrity_errors,
65 | gino_implementation_integrity_errors,
66 | ],
67 | scope="function",
68 | )
69 | def integrity_errors_client(request):
70 | yield from yield_test_client(request.param(), request.param)
71 |
--------------------------------------------------------------------------------
/tests/dev.requirements.txt:
--------------------------------------------------------------------------------
1 | fastapi
2 | httpx
3 |
4 | # Backends
5 | ormar
6 | tortoise-orm==0.18.1
7 | databases
8 | aiosqlite
9 | sqlalchemy<1.4.0
10 | sqlalchemy_utils==0.36.8
11 | gino-starlette==0.1.1
12 | asyncpg
13 |
14 | # Testing
15 | pytest
16 | pytest-virtualenv
17 | requests
18 | asynctest
19 | psycopg2
20 | pyodbc
21 |
22 | # Linting
23 | flake8
24 | flake8-black
25 | flake8-bugbear
26 | flake8-import-order
27 | flake8-bandit
28 | flake8-annotations
29 | flake8-builtins
30 | flake8-variables-names
31 | flake8-functions
32 | flake8-expression-complexity
33 |
34 | # Typing
35 | mypy==0.910
36 |
--------------------------------------------------------------------------------
/tests/implementations/__init__.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | from .databases_ import (
4 | databases_implementation,
5 | databases_implementation_custom_ids,
6 | databases_implementation_string_pk,
7 | )
8 | from .gino_ import (
9 | gino_implementation,
10 | gino_implementation_custom_ids,
11 | gino_implementation_integrity_errors,
12 | gino_implementation_string_pk,
13 | )
14 | from .memory import memory_implementation
15 | from .ormar_ import (
16 | ormar_implementation,
17 | ormar_implementation_custom_ids,
18 | ormar_implementation_integrity_errors,
19 | ormar_implementation_string_pk,
20 | )
21 | from .sqlalchemy_ import (
22 | sqlalchemy_implementation,
23 | sqlalchemy_implementation_custom_ids,
24 | sqlalchemy_implementation_integrity_errors,
25 | sqlalchemy_implementation_string_pk,
26 | DSN_LIST,
27 | )
28 | from .tortoise_ import tortoise_implementation
29 |
30 | implementations = [
31 | (memory_implementation, ""),
32 | (ormar_implementation, ""),
33 | (gino_implementation, ""),
34 | ]
35 |
36 | implementations.extend([(sqlalchemy_implementation, dsn) for dsn in DSN_LIST])
37 | implementations.extend([(databases_implementation, dsn) for dsn in DSN_LIST])
38 |
39 |
40 | if sys.version_info >= (3, 8):
41 | implementations.append((tortoise_implementation, ""))
42 |
--------------------------------------------------------------------------------
/tests/implementations/databases_.py:
--------------------------------------------------------------------------------
1 | import databases
2 | from fastapi import FastAPI
3 | from sqlalchemy import Column, Float, Integer, MetaData, String, Table, create_engine
4 | from sqlalchemy_utils import create_database, database_exists, drop_database
5 |
6 | from fastapi_crudrouter import DatabasesCRUDRouter
7 | from tests import (
8 | Carrot,
9 | CarrotCreate,
10 | CarrotUpdate,
11 | CustomPotato,
12 | PAGINATION_SIZE,
13 | Potato,
14 | PotatoType,
15 | CUSTOM_TAGS,
16 | config,
17 | )
18 |
19 | DSN_LIST = [
20 | "sqlite:///./test.db?check_same_thread=false",
21 | # config.MSSQL_URI,
22 | config.POSTGRES_URI,
23 | ]
24 |
25 |
26 | def _setup_database(db_uri: str = DSN_LIST[0]):
27 | if database_exists(db_uri):
28 | drop_database(db_uri)
29 |
30 | create_database(db_uri)
31 | database = databases.Database(db_uri)
32 | engine = create_engine(db_uri)
33 |
34 | return engine, database
35 |
36 |
37 | def databases_implementation(db_uri: str):
38 | engine, database = _setup_database(db_uri)
39 |
40 | metadata = MetaData()
41 | potatoes = Table(
42 | "potatoes",
43 | metadata,
44 | Column("id", Integer, primary_key=True),
45 | Column("thickness", Float),
46 | Column("mass", Float),
47 | Column("color", String),
48 | Column("type", String),
49 | )
50 | carrots = Table(
51 | "carrots",
52 | metadata,
53 | Column("id", Integer, primary_key=True),
54 | Column("length", Float),
55 | Column("color", String),
56 | )
57 | metadata.create_all(bind=engine)
58 |
59 | app = FastAPI()
60 |
61 | @app.on_event("startup")
62 | async def startup():
63 | await database.connect()
64 |
65 | @app.on_event("shutdown")
66 | async def shutdown():
67 | await database.disconnect()
68 |
69 | router_settings = [
70 | dict(
71 | database=database,
72 | table=potatoes,
73 | schema=Potato,
74 | prefix="potato",
75 | paginate=PAGINATION_SIZE,
76 | ),
77 | dict(
78 | database=database,
79 | table=carrots,
80 | schema=Carrot,
81 | create_schema=CarrotCreate,
82 | update_schema=CarrotUpdate,
83 | prefix="carrot",
84 | tags=CUSTOM_TAGS,
85 | ),
86 | ]
87 |
88 | return app, DatabasesCRUDRouter, router_settings
89 |
90 |
91 | def databases_implementation_custom_ids():
92 | engine, database = _setup_database()
93 |
94 | metadata = MetaData()
95 | potatoes = Table(
96 | "potatoes",
97 | metadata,
98 | Column("potato_id", Integer, primary_key=True),
99 | Column("thickness", Float),
100 | Column("mass", Float),
101 | Column("color", String),
102 | Column("type", String),
103 | )
104 |
105 | metadata.create_all(bind=engine)
106 |
107 | app = FastAPI()
108 |
109 | @app.on_event("startup")
110 | async def startup():
111 | await database.connect()
112 |
113 | @app.on_event("shutdown")
114 | async def shutdown():
115 | await database.disconnect()
116 |
117 | potato_router = DatabasesCRUDRouter(
118 | database=database, table=potatoes, schema=CustomPotato
119 | )
120 | app.include_router(potato_router)
121 |
122 | return app
123 |
124 |
125 | def databases_implementation_string_pk():
126 | engine, database = _setup_database()
127 |
128 | metadata = MetaData()
129 | potato_types = Table(
130 | "potato_type",
131 | metadata,
132 | Column("name", String, primary_key=True),
133 | Column("origin", String),
134 | )
135 |
136 | metadata.create_all(bind=engine)
137 |
138 | app = FastAPI()
139 |
140 | @app.on_event("startup")
141 | async def startup():
142 | await database.connect()
143 |
144 | @app.on_event("shutdown")
145 | async def shutdown():
146 | await database.disconnect()
147 |
148 | potato_router = DatabasesCRUDRouter(
149 | database=database,
150 | table=potato_types,
151 | schema=PotatoType,
152 | create_schema=PotatoType,
153 | )
154 | app.include_router(potato_router)
155 |
156 | return app
157 |
--------------------------------------------------------------------------------
/tests/implementations/gino_.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | from fastapi import FastAPI
3 | from fastapi_crudrouter import GinoCRUDRouter
4 | from gino.ext.starlette import Gino
5 | from sqlalchemy_utils import create_database, database_exists, drop_database
6 | from tests import (
7 | CUSTOM_TAGS,
8 | PAGINATION_SIZE,
9 | Carrot,
10 | CarrotCreate,
11 | CarrotUpdate,
12 | CustomPotato,
13 | Potato,
14 | PotatoType,
15 | config,
16 | )
17 |
18 |
19 | GINO_DATABASE_URL = config.POSTGRES_URI.replace("postgresql", "asyncpg")
20 |
21 |
22 | async def migrate(db):
23 | async with db.with_bind(GINO_DATABASE_URL):
24 | await db.gino.create_all()
25 |
26 |
27 | def _setup_base_app():
28 | if database_exists(config.POSTGRES_URI):
29 | drop_database(config.POSTGRES_URI)
30 |
31 | create_database(config.POSTGRES_URI)
32 |
33 | app = FastAPI()
34 | db = Gino(dsn=GINO_DATABASE_URL)
35 | db.init_app(app)
36 | return db, app
37 |
38 |
39 | def gino_implementation(**kwargs):
40 | db, app = _setup_base_app()
41 |
42 | class PotatoModel(db.Model):
43 | __tablename__ = "potatoes"
44 | id = db.Column(db.Integer, primary_key=True, index=True)
45 | thickness = db.Column(db.Float)
46 | mass = db.Column(db.Float)
47 | color = db.Column(db.String)
48 | type = db.Column(db.String)
49 |
50 | class CarrotModel(db.Model):
51 | __tablename__ = "carrots"
52 | id = db.Column(db.Integer, primary_key=True, index=True)
53 | length = db.Column(db.Float)
54 | color = db.Column(db.String)
55 |
56 | asyncio.get_event_loop().run_until_complete(migrate(db))
57 |
58 | router_settings = [
59 | dict(
60 | schema=Potato,
61 | db_model=PotatoModel,
62 | db=db,
63 | prefix="potato",
64 | paginate=PAGINATION_SIZE,
65 | ),
66 | dict(
67 | schema=Carrot,
68 | db_model=CarrotModel,
69 | db=db,
70 | create_schema=CarrotCreate,
71 | update_schema=CarrotUpdate,
72 | prefix="carrot",
73 | tags=CUSTOM_TAGS,
74 | ),
75 | ]
76 |
77 | return app, GinoCRUDRouter, router_settings
78 |
79 |
80 | # noinspection DuplicatedCode
81 | def gino_implementation_custom_ids():
82 | db, app = _setup_base_app()
83 |
84 | class PotatoModel(db.Model):
85 | __tablename__ = "potatoes"
86 | potato_id = db.Column(db.Integer, primary_key=True, index=True)
87 | thickness = db.Column(db.Float)
88 | mass = db.Column(db.Float)
89 | color = db.Column(db.String)
90 | type = db.Column(db.String)
91 |
92 | asyncio.get_event_loop().run_until_complete(migrate(db))
93 |
94 | app.include_router(GinoCRUDRouter(schema=CustomPotato, db_model=PotatoModel, db=db))
95 |
96 | return app
97 |
98 |
99 | def gino_implementation_string_pk():
100 | db, app = _setup_base_app()
101 |
102 | class PotatoTypeModel(db.Model):
103 | __tablename__ = "potato_type"
104 | name = db.Column(db.String, primary_key=True, index=True)
105 | origin = db.Column(db.String)
106 |
107 | asyncio.get_event_loop().run_until_complete(migrate(db))
108 |
109 | app.include_router(
110 | GinoCRUDRouter(
111 | schema=PotatoType,
112 | create_schema=PotatoType,
113 | db_model=PotatoTypeModel,
114 | db=db,
115 | prefix="potato_type",
116 | )
117 | )
118 |
119 | return app
120 |
121 |
122 | def gino_implementation_integrity_errors():
123 | db, app = _setup_base_app()
124 |
125 | class PotatoModel(db.Model):
126 | __tablename__ = "potatoes"
127 | id = db.Column(db.Integer, primary_key=True, index=True)
128 | thickness = db.Column(db.Float)
129 | mass = db.Column(db.Float)
130 | color = db.Column(db.String, unique=True)
131 | type = db.Column(db.String)
132 |
133 | class CarrotModel(db.Model):
134 | __tablename__ = "carrots"
135 | id = db.Column(db.Integer, primary_key=True, index=True)
136 | length = db.Column(db.Float)
137 | color = db.Column(db.String)
138 |
139 | asyncio.get_event_loop().run_until_complete(migrate(db))
140 |
141 | app.include_router(
142 | GinoCRUDRouter(
143 | schema=Potato,
144 | db_model=PotatoModel,
145 | db=db,
146 | create_schema=Potato,
147 | prefix="potatoes",
148 | )
149 | )
150 | app.include_router(
151 | GinoCRUDRouter(
152 | schema=Carrot,
153 | db_model=CarrotModel,
154 | db=db,
155 | update_schema=CarrotUpdate,
156 | prefix="carrots",
157 | )
158 | )
159 |
160 | return app
161 |
--------------------------------------------------------------------------------
/tests/implementations/memory.py:
--------------------------------------------------------------------------------
1 | from fastapi import FastAPI
2 |
3 | from fastapi_crudrouter import MemoryCRUDRouter
4 | from tests import Potato, Carrot, CarrotUpdate, PAGINATION_SIZE, CUSTOM_TAGS
5 |
6 |
7 | def memory_implementation(**kwargs):
8 | app = FastAPI()
9 | router_settings = [
10 | dict(schema=Potato, paginate=PAGINATION_SIZE),
11 | dict(schema=Carrot, update_schema=CarrotUpdate, tags=CUSTOM_TAGS),
12 | ]
13 |
14 | return app, MemoryCRUDRouter, router_settings
15 |
16 |
17 | if __name__ == "__main__":
18 | import uvicorn
19 |
20 | uvicorn.run(memory_implementation(), port=5000)
21 |
--------------------------------------------------------------------------------
/tests/implementations/ormar_.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import databases
4 | import ormar
5 | import pytest
6 | import sqlalchemy
7 | from fastapi import FastAPI
8 |
9 | from fastapi_crudrouter import OrmarCRUDRouter
10 | from tests import CarrotCreate, CarrotUpdate, PAGINATION_SIZE, CUSTOM_TAGS
11 |
12 | DATABASE_URL = "sqlite:///./test.db"
13 | database = databases.Database(DATABASE_URL)
14 | metadata = sqlalchemy.MetaData()
15 |
16 |
17 | @pytest.fixture(scope="function", autouse=True)
18 | async def cleanup():
19 | async with database:
20 | await PotatoModel.objects.delete(each=True)
21 | await CarrotModel.objects.delete(each=True)
22 |
23 |
24 | class BaseMeta(ormar.ModelMeta):
25 | metadata = metadata
26 | database = database
27 |
28 |
29 | def _setup_database():
30 | engine = sqlalchemy.create_engine(DATABASE_URL)
31 | metadata.drop_all(engine)
32 | metadata.create_all(engine)
33 | return engine, database
34 |
35 |
36 | class PotatoModel(ormar.Model):
37 | class Meta(BaseMeta):
38 | pass
39 |
40 | id = ormar.Integer(primary_key=True)
41 | thickness = ormar.Float()
42 | mass = ormar.Float()
43 | color = ormar.String(max_length=255)
44 | type = ormar.String(max_length=255)
45 |
46 |
47 | class CarrotModel(ormar.Model):
48 | class Meta(BaseMeta):
49 | pass
50 |
51 | id = ormar.Integer(primary_key=True)
52 | length = ormar.Float()
53 | color = ormar.String(max_length=255)
54 |
55 |
56 | class PotatoTypeModel(ormar.Model):
57 | class Meta(BaseMeta):
58 | tablename = "potato_type"
59 |
60 | name = ormar.String(primary_key=True, max_length=300)
61 | origin = ormar.String(max_length=300)
62 |
63 |
64 | class CustomPotatoModel(ormar.Model):
65 | class Meta(BaseMeta):
66 | tablename = "custom_potatoes"
67 |
68 | potato_id = ormar.Integer(primary_key=True)
69 | thickness = ormar.Float()
70 | mass = ormar.Float()
71 | color = ormar.String(max_length=255)
72 | type = ormar.String(max_length=255)
73 |
74 |
75 | class UniquePotatoModel(ormar.Model):
76 | class Meta(BaseMeta):
77 | pass
78 |
79 | id = ormar.Integer(primary_key=True)
80 | thickness = ormar.Float()
81 | mass = ormar.Float()
82 | color = ormar.String(max_length=255, unique=True)
83 | type = ormar.String(max_length=255)
84 |
85 |
86 | def get_app():
87 | [
88 | os.remove(f"./db.sqlite3{s}")
89 | for s in ["", "-wal", "-shm"]
90 | if os.path.exists(f"./db.sqlite3{s}")
91 | ]
92 |
93 | _setup_database()
94 |
95 | app = FastAPI()
96 |
97 | @app.on_event("startup")
98 | async def startup():
99 | await database.connect()
100 |
101 | @app.on_event("shutdown")
102 | async def shutdown():
103 | await database.disconnect()
104 |
105 | return app
106 |
107 |
108 | def ormar_implementation(**kwargs):
109 | app = get_app()
110 |
111 | router_settings = [
112 | dict(
113 | schema=PotatoModel,
114 | prefix="potato",
115 | paginate=PAGINATION_SIZE,
116 | ),
117 | dict(
118 | schema=CarrotModel,
119 | update_schema=CarrotUpdate,
120 | prefix="carrot",
121 | tags=CUSTOM_TAGS,
122 | ),
123 | ]
124 |
125 | return (
126 | app,
127 | OrmarCRUDRouter,
128 | router_settings,
129 | )
130 |
131 |
132 | # noinspection DuplicatedCode
133 | def ormar_implementation_custom_ids():
134 | app = get_app()
135 |
136 | app.include_router(
137 | OrmarCRUDRouter(
138 | schema=CustomPotatoModel,
139 | prefix="potatoes",
140 | paginate=PAGINATION_SIZE,
141 | )
142 | )
143 |
144 | return app
145 |
146 |
147 | def ormar_implementation_string_pk():
148 | app = get_app()
149 |
150 | app.include_router(
151 | OrmarCRUDRouter(
152 | schema=PotatoTypeModel,
153 | prefix="potato_type",
154 | )
155 | )
156 |
157 | return app
158 |
159 |
160 | def ormar_implementation_integrity_errors():
161 | app = get_app()
162 |
163 | app.include_router(
164 | OrmarCRUDRouter(
165 | schema=UniquePotatoModel,
166 | prefix="potatoes",
167 | paginate=PAGINATION_SIZE,
168 | )
169 | )
170 | app.include_router(
171 | OrmarCRUDRouter(
172 | schema=CarrotModel,
173 | create_schema=CarrotCreate,
174 | update_schema=CarrotUpdate,
175 | prefix="carrots",
176 | )
177 | )
178 |
179 | return app
180 |
--------------------------------------------------------------------------------
/tests/implementations/sqlalchemy_.py:
--------------------------------------------------------------------------------
1 | from fastapi import FastAPI
2 | from sqlalchemy import Column, Float, Integer, String
3 | from sqlalchemy import create_engine
4 | from sqlalchemy.ext.declarative import declarative_base
5 | from sqlalchemy.orm import sessionmaker
6 | from sqlalchemy_utils import create_database, database_exists, drop_database
7 |
8 | from fastapi_crudrouter import SQLAlchemyCRUDRouter
9 | from tests import (
10 | Carrot,
11 | CarrotCreate,
12 | CarrotUpdate,
13 | CustomPotato,
14 | PAGINATION_SIZE,
15 | Potato,
16 | PotatoType,
17 | CUSTOM_TAGS,
18 | config,
19 | )
20 |
21 | DSN_LIST = [
22 | "sqlite:///./test.db?check_same_thread=false",
23 | # config.MSSQL_URI,
24 | config.POSTGRES_URI,
25 | ]
26 |
27 |
28 | def _setup_base_app(db_uri: str = DSN_LIST[0]):
29 | if database_exists(db_uri):
30 | drop_database(db_uri)
31 |
32 | create_database(db_uri)
33 |
34 | app = FastAPI()
35 |
36 | engine = create_engine(db_uri)
37 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
38 | Base = declarative_base()
39 |
40 | def session():
41 | session = SessionLocal()
42 | try:
43 | yield session
44 | session.commit()
45 | finally:
46 | session.close()
47 |
48 | return app, engine, Base, session
49 |
50 |
51 | def sqlalchemy_implementation(db_uri: str):
52 | app, engine, Base, session = _setup_base_app(db_uri)
53 |
54 | class PotatoModel(Base):
55 | __tablename__ = "potatoes"
56 | id = Column(Integer, primary_key=True, index=True)
57 | thickness = Column(Float)
58 | mass = Column(Float)
59 | color = Column(String)
60 | type = Column(String)
61 |
62 | class CarrotModel(Base):
63 | __tablename__ = "carrots"
64 | id = Column(Integer, primary_key=True, index=True)
65 | length = Column(Float)
66 | color = Column(String)
67 |
68 | Base.metadata.create_all(bind=engine)
69 | router_settings = [
70 | dict(
71 | schema=Potato,
72 | db_model=PotatoModel,
73 | db=session,
74 | prefix="potato",
75 | paginate=PAGINATION_SIZE,
76 | ),
77 | dict(
78 | schema=Carrot,
79 | db_model=CarrotModel,
80 | db=session,
81 | create_schema=CarrotCreate,
82 | update_schema=CarrotUpdate,
83 | prefix="carrot",
84 | tags=CUSTOM_TAGS,
85 | ),
86 | ]
87 |
88 | return app, SQLAlchemyCRUDRouter, router_settings
89 |
90 |
91 | # noinspection DuplicatedCode
92 | def sqlalchemy_implementation_custom_ids():
93 | app, engine, Base, session = _setup_base_app()
94 |
95 | class PotatoModel(Base):
96 | __tablename__ = "potatoes"
97 | potato_id = Column(Integer, primary_key=True, index=True)
98 | thickness = Column(Float)
99 | mass = Column(Float)
100 | color = Column(String)
101 | type = Column(String)
102 |
103 | Base.metadata.create_all(bind=engine)
104 | app.include_router(
105 | SQLAlchemyCRUDRouter(schema=CustomPotato, db_model=PotatoModel, db=session)
106 | )
107 |
108 | return app
109 |
110 |
111 | def sqlalchemy_implementation_string_pk():
112 | app, engine, Base, session = _setup_base_app()
113 |
114 | class PotatoTypeModel(Base):
115 | __tablename__ = "potato_type"
116 | name = Column(String, primary_key=True, index=True)
117 | origin = Column(String)
118 |
119 | Base.metadata.create_all(bind=engine)
120 | app.include_router(
121 | SQLAlchemyCRUDRouter(
122 | schema=PotatoType,
123 | create_schema=PotatoType,
124 | db_model=PotatoTypeModel,
125 | db=session,
126 | prefix="potato_type",
127 | )
128 | )
129 |
130 | return app
131 |
132 |
133 | def sqlalchemy_implementation_integrity_errors():
134 | app, engine, Base, session = _setup_base_app()
135 |
136 | class PotatoModel(Base):
137 | __tablename__ = "potatoes"
138 | id = Column(Integer, primary_key=True, index=True)
139 | thickness = Column(Float)
140 | mass = Column(Float)
141 | color = Column(String, unique=True)
142 | type = Column(String)
143 |
144 | class CarrotModel(Base):
145 | __tablename__ = "carrots"
146 | id = Column(Integer, primary_key=True, index=True)
147 | length = Column(Float)
148 | color = Column(String)
149 |
150 | Base.metadata.create_all(bind=engine)
151 | app.include_router(
152 | SQLAlchemyCRUDRouter(
153 | schema=Potato,
154 | db_model=PotatoModel,
155 | db=session,
156 | create_schema=Potato,
157 | prefix="potatoes",
158 | )
159 | )
160 | app.include_router(
161 | SQLAlchemyCRUDRouter(
162 | schema=Carrot,
163 | db_model=CarrotModel,
164 | db=session,
165 | update_schema=CarrotUpdate,
166 | prefix="carrots",
167 | )
168 | )
169 |
170 | return app
171 |
--------------------------------------------------------------------------------
/tests/implementations/tortoise_.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from fastapi import FastAPI
4 | from tortoise import Model, Tortoise, fields
5 |
6 | from fastapi_crudrouter import TortoiseCRUDRouter
7 | from tests import (
8 | Carrot,
9 | CarrotCreate,
10 | CarrotUpdate,
11 | PAGINATION_SIZE,
12 | Potato,
13 | CUSTOM_TAGS,
14 | )
15 |
16 |
17 | class PotatoModel(Model):
18 | thickness = fields.FloatField()
19 | mass = fields.FloatField()
20 | color = fields.CharField(max_length=255)
21 | type = fields.CharField(max_length=255)
22 |
23 |
24 | class CarrotModel(Model):
25 | length = fields.FloatField()
26 | color = fields.CharField(max_length=255)
27 |
28 |
29 | TORTOISE_ORM = {
30 | "connections": {"default": "sqlite://db.sqlite3"},
31 | "apps": {
32 | "models": {
33 | "models": ["tests.implementations.tortoise_"],
34 | "default_connection": "default",
35 | },
36 | },
37 | }
38 |
39 |
40 | async def on_shutdown():
41 | await Tortoise.close_connections()
42 |
43 |
44 | def tortoise_implementation(**kwargs):
45 | [
46 | os.remove(f"./db.sqlite3{s}")
47 | for s in ["", "-wal", "-shm"]
48 | if os.path.exists(f"./db.sqlite3{s}")
49 | ]
50 |
51 | app = FastAPI(on_shutdown=[on_shutdown])
52 |
53 | Tortoise.init(config=TORTOISE_ORM)
54 | Tortoise.generate_schemas()
55 |
56 | router_settings = [
57 | dict(
58 | schema=Potato,
59 | db_model=PotatoModel,
60 | prefix="potato",
61 | paginate=PAGINATION_SIZE,
62 | ),
63 | dict(
64 | schema=Carrot,
65 | db_model=CarrotModel,
66 | create_schema=CarrotCreate,
67 | update_schema=CarrotUpdate,
68 | prefix="carrot",
69 | tags=CUSTOM_TAGS,
70 | ),
71 | ]
72 |
73 | return app, TortoiseCRUDRouter, router_settings
74 |
--------------------------------------------------------------------------------
/tests/test_base.py:
--------------------------------------------------------------------------------
1 | from abc import ABC
2 | from typing import Type
3 |
4 | import pytest
5 | from fastapi import APIRouter, FastAPI
6 |
7 | from fastapi_crudrouter import (
8 | GinoCRUDRouter,
9 | MemoryCRUDRouter,
10 | OrmarCRUDRouter,
11 | SQLAlchemyCRUDRouter,
12 | DatabasesCRUDRouter,
13 | )
14 |
15 | # noinspection PyProtectedMember
16 | from fastapi_crudrouter.core._base import CRUDGenerator
17 | from tests import Potato
18 |
19 |
20 | @pytest.fixture(
21 | params=[
22 | GinoCRUDRouter,
23 | SQLAlchemyCRUDRouter,
24 | MemoryCRUDRouter,
25 | OrmarCRUDRouter,
26 | GinoCRUDRouter,
27 | DatabasesCRUDRouter,
28 | ]
29 | )
30 | def subclass(request) -> Type[CRUDGenerator]:
31 | return request.param
32 |
33 |
34 | def test_router_is_subclass_of_crud_generator(subclass):
35 | assert issubclass(subclass, CRUDGenerator)
36 |
37 |
38 | def test_router_is_subclass_of_api_router(subclass):
39 | assert issubclass(subclass, APIRouter)
40 |
41 |
42 | def test_base_class_is_abstract():
43 | assert issubclass(CRUDGenerator, ABC)
44 |
45 |
46 | def test_raise_not_implemented():
47 | app = FastAPI()
48 |
49 | def foo(*args, **kwargs):
50 | def bar():
51 | pass
52 |
53 | return bar
54 |
55 | methods = CRUDGenerator.get_routes()
56 |
57 | for m in methods:
58 | with pytest.raises(TypeError):
59 | app.include_router(CRUDGenerator(schema=Potato))
60 |
61 | setattr(CRUDGenerator, f"_{m}", foo)
62 |
--------------------------------------------------------------------------------
/tests/test_custom_ids.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from . import test_router
4 |
5 | basic_potato = dict(potato_id=1, thickness=0.24, mass=1.2, color="Brown", type="Russet")
6 |
7 | PotatoUrl = "/potatoes"
8 |
9 |
10 | def test_get(custom_id_client):
11 | test_router.test_get(custom_id_client, PotatoUrl)
12 |
13 |
14 | def test_post(custom_id_client):
15 | test_router.test_post(custom_id_client, PotatoUrl, basic_potato)
16 |
17 |
18 | def test_get_one(custom_id_client):
19 | test_router.test_get_one(
20 | custom_id_client, PotatoUrl, basic_potato, id_key="potato_id"
21 | )
22 |
23 |
24 | def test_update(custom_id_client):
25 | test_router.test_update(
26 | custom_id_client, PotatoUrl, basic_potato, id_key="potato_id"
27 | )
28 |
29 |
30 | def test_delete_one(custom_id_client):
31 | test_router.test_delete_one(
32 | custom_id_client, PotatoUrl, basic_potato, id_key="potato_id"
33 | )
34 |
35 |
36 | def test_delete_all(custom_id_client):
37 | test_router.test_delete_all(custom_id_client, PotatoUrl, basic_potato)
38 |
39 |
40 | @pytest.mark.parametrize("id_", [-1, 0, 4, "14"])
41 | def test_not_found(custom_id_client, id_):
42 | test_router.test_not_found(custom_id_client, id_, PotatoUrl, basic_potato)
43 |
--------------------------------------------------------------------------------
/tests/test_dependencies/test_disable.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from fastapi_crudrouter.core import CRUDGenerator
4 |
5 | from tests.implementations import implementations
6 | from tests.conftest import yield_test_client, label_func
7 | from tests import test_router
8 |
9 |
10 | URLS = ["/potato", "/carrot"]
11 | AUTH = {"Authorization": "Bearer my_token"}
12 | KEY_WORDS = {f"{r}_route" for r in CRUDGenerator.get_routes()}
13 | DISABLE_KWARGS = {k: False for k in KEY_WORDS}
14 |
15 |
16 | @pytest.fixture(params=implementations, ids=label_func, scope="class")
17 | def client(request):
18 | impl, dsn = request.param
19 |
20 | app, router, settings = impl(db_uri=dsn)
21 | [app.include_router(router(**s, **DISABLE_KWARGS)) for s in settings]
22 |
23 | yield from yield_test_client(app, impl)
24 |
25 |
26 | @pytest.fixture(params=implementations, ids=label_func, scope="class")
27 | def delete_all_client(request):
28 | impl, dsn = request.param
29 |
30 | app, router, settings = impl(db_uri=dsn)
31 | [
32 | app.include_router(router(**s, delete_all_route=False, update_route=False))
33 | for s in settings
34 | ]
35 |
36 | yield from yield_test_client(app, impl)
37 |
38 |
39 | @pytest.mark.parametrize("url", URLS)
40 | def test_route_disable(client, url):
41 | assert client.get(url).status_code == 404
42 | assert client.get(url).status_code == 404
43 | assert client.post(url).status_code == 404
44 |
45 | for id_ in [-1, 1, 0, 14]:
46 | assert client.get(f"{url}/{id_}").status_code == 404
47 | assert client.put(f"{url}/{id_}").status_code == 404
48 | assert client.delete(f"{url}/{id_}").status_code == 404
49 |
50 |
51 | def test_route_disable_single(delete_all_client):
52 | url = "/potato"
53 |
54 | assert delete_all_client.delete(url).status_code == 405
55 |
56 | test_router.test_post(delete_all_client, url)
57 | assert delete_all_client.put(f"{url}/{1}").status_code == 405
58 | assert delete_all_client.delete(f"{url}/{1}").status_code == 200
59 |
--------------------------------------------------------------------------------
/tests/test_dependencies/test_per_route.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from fastapi import Depends, HTTPException
3 |
4 | from tests.conftest import yield_test_client
5 | from tests.implementations import implementations
6 |
7 | URLS = ["/potato", "/carrot"]
8 | AUTH = {"Authorization": "Bearer my_token"}
9 |
10 |
11 | class RaisesException:
12 | def __init__(self, status_code: int):
13 | self.status_code = status_code
14 |
15 | def __call__(self):
16 | raise HTTPException(self.status_code)
17 |
18 |
19 | class NullDependency:
20 | def __init__(self, status_code: int):
21 | self.status_code = status_code
22 |
23 | def __call__(self):
24 | pass
25 |
26 |
27 | DEPENDS_KWARGS = dict(
28 | get_all_route=[Depends(NullDependency), Depends(RaisesException(401))],
29 | get_one_route=[Depends(NullDependency), Depends(RaisesException(402))],
30 | create_route=[Depends(NullDependency), Depends(RaisesException(403))],
31 | update_route=[Depends(NullDependency), Depends(RaisesException(404))],
32 | delete_all_route=[Depends(NullDependency), Depends(RaisesException(405))],
33 | delete_one_route=[Depends(NullDependency), Depends(RaisesException(406))],
34 | )
35 |
36 |
37 | @pytest.fixture(params=implementations)
38 | def client(request):
39 | impl, dsn = request.param
40 |
41 | app, router, settings = impl(db_uri=dsn)
42 | [app.include_router(router(**s, **DEPENDS_KWARGS)) for s in settings]
43 |
44 | yield from yield_test_client(app, impl)
45 |
46 |
47 | @pytest.mark.parametrize("url", URLS)
48 | def test_route_disable(client, url):
49 | item_url = f"{url}/1"
50 | actions = [
51 | (client.get, url),
52 | (client.get, item_url),
53 | (client.post, url),
54 | (client.put, item_url),
55 | (client.delete, url),
56 | (client.delete, item_url),
57 | ]
58 |
59 | err_codes = set()
60 | for method, url in actions:
61 | print(method, url, err_codes)
62 | status_code = method(url).status_code
63 |
64 | assert status_code != 200
65 | assert 400 <= status_code <= 406
66 | assert status_code not in err_codes
67 |
68 | err_codes.add(status_code)
69 |
70 | assert len(err_codes) == len(actions)
71 |
--------------------------------------------------------------------------------
/tests/test_dependencies/test_top_level.py:
--------------------------------------------------------------------------------
1 | from fastapi import Depends, HTTPException
2 | from fastapi.security import OAuth2PasswordBearer
3 |
4 | import pytest
5 |
6 | from tests.implementations import implementations
7 | from tests.conftest import yield_test_client
8 |
9 | URLS = ["/potato", "/carrot"]
10 | AUTH = {"Authorization": "Bearer my_token"}
11 |
12 |
13 | @pytest.fixture(params=implementations, scope="class")
14 | def client(request):
15 | impl, dsn = request.param
16 |
17 | oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/token")
18 |
19 | def token_auth(token: str = Depends(oauth2_scheme)):
20 | if not token:
21 | raise HTTPException(401, "Invalid token")
22 |
23 | app, router, settings = impl(db_uri=dsn)
24 | [
25 | app.include_router(router(**s, dependencies=[Depends(token_auth)]))
26 | for s in settings
27 | ]
28 |
29 | yield from yield_test_client(app, impl)
30 |
31 |
32 | @pytest.fixture(params=URLS)
33 | def url(request):
34 | yield request.param
35 |
36 |
37 | class TestTopLevelDependencies:
38 | @staticmethod
39 | def test_authorization(client, url):
40 | assert client.get(url, headers=AUTH).status_code == 200
41 | assert client.post(url, headers=AUTH).status_code != 401
42 | assert client.delete(url, headers=AUTH).status_code == 200
43 |
44 | for id_ in [-1, 1, 0, 14]:
45 | assert client.get(f"{url}/{id_}", headers=AUTH).status_code != 401
46 | assert client.put(f"{url}/{id_}", headers=AUTH).status_code != 401
47 | assert client.delete(f"{url}/{id_}", headers=AUTH).status_code != 401
48 |
49 | @staticmethod
50 | def test_authorization_fail(client, url):
51 | assert client.get(url).status_code == 401
52 | assert client.get(url).status_code == 401
53 | assert client.post(url).status_code == 401
54 |
55 | for id_ in [-1, 1, 0, 14]:
56 | assert client.get(f"{url}/{id_}").status_code == 401
57 | assert client.put(f"{url}/{id_}").status_code == 401
58 | assert client.delete(f"{url}/{id_}").status_code == 401
59 |
--------------------------------------------------------------------------------
/tests/test_exclude.py:
--------------------------------------------------------------------------------
1 | import random
2 | import pytest
3 |
4 | from fastapi import FastAPI, testclient
5 | from fastapi_crudrouter import MemoryCRUDRouter
6 |
7 | from tests import Potato
8 |
9 | URL = "/potato"
10 |
11 |
12 | def get_client(**kwargs):
13 | app = FastAPI()
14 | app.include_router(MemoryCRUDRouter(schema=Potato, prefix=URL, **kwargs))
15 |
16 | return testclient.TestClient(app)
17 |
18 |
19 | @pytest.mark.parametrize("i", list(range(1, len(MemoryCRUDRouter.get_routes()) + 1)))
20 | def test_exclude_internal(i):
21 | keys = random.sample(MemoryCRUDRouter.get_routes(), k=i)
22 | kwargs = {r + "_route": False for r in keys}
23 |
24 | router = MemoryCRUDRouter(schema=Potato, prefix=URL, **kwargs)
25 | assert len(router.routes) == len(MemoryCRUDRouter.get_routes()) - i
26 |
27 |
28 | def test_exclude_delete_all():
29 | client = get_client(delete_all_route=False)
30 | assert client.delete(URL).status_code == 405
31 | assert client.get(URL).status_code == 200
32 |
33 |
34 | def test_exclude_all():
35 | routes = MemoryCRUDRouter.get_routes()
36 | kwargs = {r + "_route": False for r in routes}
37 | client = get_client(**kwargs)
38 |
39 | assert client.delete(URL).status_code == 404
40 | assert client.get(URL).status_code == 404
41 | assert client.post(URL).status_code == 404
42 | assert client.put(URL).status_code == 404
43 |
44 | for id_ in [-1, 1, 0, 14]:
45 | assert client.get(f"{URL}/{id_}").status_code == 404
46 | assert client.post(f"{URL}/{id_}").status_code == 404
47 | assert client.put(f"{URL}/{id_}").status_code == 404
48 | assert client.delete(f"{URL}/{id_}").status_code == 404
49 |
--------------------------------------------------------------------------------
/tests/test_integration/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awtkns/fastapi-crudrouter/9b829865d85113a3f16f94c029502a9a584d47bb/tests/test_integration/__init__.py
--------------------------------------------------------------------------------
/tests/test_integration/test_backend_not_installed.py:
--------------------------------------------------------------------------------
1 | import pathlib
2 |
3 |
4 | def test_virtualenv(virtualenv):
5 | file = pathlib.Path(__file__)
6 | package = file.parent.parent.parent
7 | assert (package / "fastapi_crudrouter").exists()
8 |
9 | virtualenv.run(f"pip install -e {package}")
10 | virtualenv.run(f"python {file}")
11 |
12 |
13 | if __name__ == "__main__":
14 | from fastapi_crudrouter import (
15 | DatabasesCRUDRouter,
16 | GinoCRUDRouter,
17 | OrmarCRUDRouter,
18 | SQLAlchemyCRUDRouter,
19 | TortoiseCRUDRouter,
20 | )
21 |
22 | routers = [
23 | SQLAlchemyCRUDRouter,
24 | DatabasesCRUDRouter,
25 | OrmarCRUDRouter,
26 | TortoiseCRUDRouter,
27 | GinoCRUDRouter,
28 | ]
29 |
30 | for crud_router in routers:
31 | try:
32 | # noinspection PyTypeChecker
33 | crud_router(..., ..., ..., ...)
34 | except AssertionError:
35 | pass
36 | except Exception:
37 | raise AssertionError(
38 | f"Not checking that all requirements are installed when initializing the {crud_router.__name__}"
39 | )
40 |
--------------------------------------------------------------------------------
/tests/test_integration/test_typing.py:
--------------------------------------------------------------------------------
1 | import pathlib
2 |
3 | file = pathlib.Path(__file__)
4 | root_dir = file.parent.parent.parent
5 | package_src = root_dir / "fastapi_crudrouter"
6 |
7 |
8 | def test_py_typed_file_exists():
9 | assert (package_src / "py.typed").exists()
10 | assert (package_src / "py.typed").is_file()
11 |
12 |
13 | def test_virtualenv(virtualenv):
14 | assert (root_dir).exists()
15 | assert (root_dir / "setup.py").exists()
16 |
17 | virtualenv.run(f"pip install -e {root_dir}")
18 | virtualenv.run(f"pip install mypy")
19 | virtualenv.run(f"mypy {file}")
20 |
21 |
22 | if __name__ == "__main__":
23 | from pydantic import BaseModel
24 | from fastapi import FastAPI
25 |
26 | import fastapi_crudrouter
27 |
28 | class User(BaseModel):
29 | id: int
30 | name: str
31 | email: str
32 |
33 | router = fastapi_crudrouter.MemoryCRUDRouter(schema=User)
34 | app = FastAPI()
35 | app.include_router(router)
36 |
--------------------------------------------------------------------------------
/tests/test_integrity_errors.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from tests import test_router
4 |
5 | POTATO_URL = "/potatoes"
6 |
7 |
8 | def test_integrity_error_create(integrity_errors_client):
9 | client = integrity_errors_client
10 | potato = dict(id=1, thickness=2, mass=5, color="red", type="russet")
11 |
12 | args = client, POTATO_URL, potato
13 | test_router.test_post(*args)
14 | with pytest.raises(AssertionError):
15 | test_router.test_post(*args)
16 |
17 | # No integrity error here because of the create_schema
18 | args = client, "/carrots", dict(id=1, length=2, color="red")
19 | test_router.test_post(*args)
20 | test_router.test_post(*args, expected_length=2)
21 |
22 |
23 | def test_integrity_error_update(integrity_errors_client):
24 | client = integrity_errors_client
25 | potato1 = dict(id=1, thickness=2, mass=5, color="red", type="russet")
26 |
27 | potato2 = dict(id=2, thickness=9, mass=5, color="yellow", type="mini")
28 |
29 | args = client, POTATO_URL
30 | test_router.test_post(*args, potato1, expected_length=1)
31 | test_router.test_post(*args, potato2, expected_length=2)
32 |
33 | potato2["color"] = potato1["color"]
34 | res = client.put(f'{POTATO_URL}/{potato2["id"]}', json=potato2)
35 | assert res.status_code == 422, res.json()
36 |
37 | potato2["color"] = "green"
38 | res = client.put(f'{POTATO_URL}/{potato2["id"]}', json=potato2)
39 | assert res.status_code == 200, res.json()
40 |
--------------------------------------------------------------------------------
/tests/test_openapi_schema.py:
--------------------------------------------------------------------------------
1 | from pytest import mark
2 |
3 | from tests import CUSTOM_TAGS
4 |
5 | POTATO_TAGS = ["Potato"]
6 | PATHS = ["/potato", "/carrot"]
7 | PATH_TAGS = {
8 | "/potato": POTATO_TAGS,
9 | "/potato/{item_id}": POTATO_TAGS,
10 | "/carrot": CUSTOM_TAGS,
11 | "/carrot/{item_id}": CUSTOM_TAGS,
12 | }
13 |
14 |
15 | class TestOpenAPISpec:
16 | def test_schema_exists(self, client):
17 | res = client.get("/openapi.json")
18 | assert res.status_code == 200
19 |
20 | return res
21 |
22 | def test_schema_tags(self, client):
23 | schema = self.test_schema_exists(client).json()
24 | paths = schema["paths"]
25 |
26 | assert len(paths) == len(PATH_TAGS)
27 | for path, method in paths.items():
28 | assert len(method) == 3
29 |
30 | for m in method:
31 | assert method[m]["tags"] == PATH_TAGS[path]
32 |
33 | @mark.parametrize("path", PATHS)
34 | def test_response_types(self, client, path):
35 | schema = self.test_schema_exists(client).json()
36 | paths = schema["paths"]
37 |
38 | for method in ["get", "post", "delete"]:
39 | assert "200" in paths[path][method]["responses"]
40 |
41 | assert "422" in paths[path]["post"]["responses"]
42 |
43 | item_path = path + "/{item_id}"
44 | for method in ["get", "put", "delete"]:
45 | assert "200" in paths[item_path][method]["responses"]
46 | assert "404" in paths[item_path][method]["responses"]
47 | assert "422" in paths[item_path][method]["responses"]
48 |
--------------------------------------------------------------------------------
/tests/test_overloads.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from fastapi import APIRouter
3 |
4 | from .implementations import implementations
5 | from .conftest import yield_test_client
6 |
7 |
8 | URLs = ["/potato", "/carrot"]
9 | PARAMS = [-1, 0, 1, 14, "ten"]
10 |
11 | GET_ALL = "Overloaded Get All"
12 | GET_ONE = "Overloaded Get One"
13 | CREATE_ONE = "Overloaded Post One"
14 | UPDATE_ONE = "Overloaded Update One"
15 | DELETE_ONE = "Overloaded Delete One"
16 | DELETE_ALL = "Overloaded Delete All"
17 | CUSTOM_ROUTE = "Custom Route"
18 |
19 |
20 | @pytest.fixture(params=implementations, scope="class")
21 | def overloaded_client(request):
22 | impl, dsn = request.param
23 |
24 | app, router, settings = impl(db_uri=dsn)
25 | routers = [router(**s) for s in settings]
26 |
27 | for r in routers:
28 | r: APIRouter
29 |
30 | @r.api_route("", methods=["GET"])
31 | def overloaded_get_all():
32 | return GET_ALL
33 |
34 | @r.get("/{item_id}")
35 | def overloaded_get_one():
36 | return GET_ONE
37 |
38 | @r.post("")
39 | def overloaded_get():
40 | return CREATE_ONE
41 |
42 | @r.put("/{item_id}")
43 | def overloaded_update():
44 | return UPDATE_ONE
45 |
46 | @r.delete("/{item_id}")
47 | def overloaded_delete():
48 | return DELETE_ONE
49 |
50 | @r.api_route("", methods=["DELETE"])
51 | def overloaded_delete():
52 | return DELETE_ALL
53 |
54 | @r.post("/custom")
55 | def custom_route():
56 | return CUSTOM_ROUTE
57 |
58 | app.include_router(r)
59 |
60 | yield from yield_test_client(app, impl)
61 |
62 |
63 | @pytest.fixture(params=URLs)
64 | def url(request):
65 | yield request.param
66 |
67 |
68 | class TestOverloads:
69 | @staticmethod
70 | def check_response(res, expected):
71 | assert res.status_code == 200
72 | assert expected in res.text
73 |
74 | def test_get_all(self, overloaded_client, url):
75 | return self.check_response(overloaded_client.get(url), GET_ALL)
76 |
77 | @pytest.mark.parametrize("id_", PARAMS)
78 | def test_get_one(self, overloaded_client, url, id_):
79 | self.check_response(overloaded_client.get(f"{url}/{id_}"), GET_ONE)
80 |
81 | def test_create(self, overloaded_client, url):
82 | self.check_response(overloaded_client.post(url), CREATE_ONE)
83 |
84 | @pytest.mark.parametrize("id_", PARAMS)
85 | def test_update(self, overloaded_client, url, id_):
86 | self.check_response(overloaded_client.put(f"{url}/{id_}"), UPDATE_ONE)
87 |
88 | @pytest.mark.parametrize("id_", PARAMS)
89 | def test_delete(self, overloaded_client, url, id_):
90 | self.check_response(overloaded_client.delete(f"{url}/{id_}"), DELETE_ONE)
91 |
92 | def test_delete_all(self, overloaded_client, url):
93 | self.check_response(overloaded_client.delete(url), DELETE_ALL)
94 |
95 | def test_custom_route(self, overloaded_client, url):
96 | self.check_response(overloaded_client.post(f"{url}/custom"), CUSTOM_ROUTE)
97 |
--------------------------------------------------------------------------------
/tests/test_pagination.py:
--------------------------------------------------------------------------------
1 | import typing
2 |
3 | import pytest
4 |
5 | from . import PAGINATION_SIZE, test_router
6 |
7 | PotatoUrl = "/potato"
8 | CarrotUrl = "/carrot"
9 | basic_carrot = dict(length=1.2, color="Orange")
10 | basic_potato = dict(thickness=0.24, mass=1.2, color="Brown", type="Russet")
11 |
12 | INSERT_COUNT = 20
13 |
14 |
15 | @pytest.fixture(scope="class")
16 | def insert_items(
17 | client,
18 | url: str = PotatoUrl,
19 | model: typing.Dict = None,
20 | count: int = INSERT_COUNT,
21 | ):
22 | model = model or basic_potato
23 | for i in range(count):
24 | test_router.test_post(
25 | client,
26 | url=url,
27 | model=model,
28 | expected_length=i + 1 if i + 1 < PAGINATION_SIZE else PAGINATION_SIZE,
29 | )
30 |
31 |
32 | @pytest.fixture(scope="class")
33 | def insert_carrots(client):
34 | for i in range(INSERT_COUNT):
35 | test_router.test_post(client, CarrotUrl, basic_carrot, expected_length=i + 1)
36 |
37 |
38 | @pytest.fixture(scope="class")
39 | def cleanup(client):
40 | yield
41 | client.delete(CarrotUrl)
42 | client.delete(PotatoUrl)
43 |
44 |
45 | def get_expected_length(limit, offset, count: int = INSERT_COUNT):
46 | expected_length = limit
47 | if offset >= count:
48 | expected_length = 0
49 |
50 | elif offset + limit >= INSERT_COUNT:
51 | expected_length = INSERT_COUNT - offset
52 |
53 | return expected_length
54 |
55 |
56 | @pytest.mark.usefixtures("insert_carrots", "insert_items", "cleanup")
57 | class TestPagination:
58 | @pytest.mark.parametrize("offset", [0, 1, 5, 10, 20, 40])
59 | @pytest.mark.parametrize("limit", [1, 5, 10])
60 | def test_pagination(self, client, limit, offset):
61 | test_router.test_get(
62 | client=client,
63 | url=PotatoUrl,
64 | params={"limit": limit, "skip": offset},
65 | expected_length=get_expected_length(limit, offset),
66 | )
67 |
68 | @pytest.mark.parametrize("offset", [-1, "asdas", 3.23])
69 | def test_invalid_offset(self, client, offset):
70 | res = client.get(PotatoUrl, params={"skip": offset})
71 | assert res.status_code == 422
72 |
73 | @pytest.mark.parametrize("limit", [-1, 0, "asdas", 3.23, 21])
74 | def test_invalid_limit(self, client, limit):
75 | res = client.get(PotatoUrl, params={"limit": limit})
76 | assert res.status_code == 422
77 |
78 | def test_pagination_disabled(self, client):
79 | test_router.test_get(client, CarrotUrl, expected_length=INSERT_COUNT)
80 |
81 | @pytest.mark.parametrize("limit", [2, 5, 10])
82 | def test_paging(self, client, limit):
83 | ids = set()
84 | skip = 0
85 | while skip < INSERT_COUNT:
86 | data = test_router.test_get(
87 | client,
88 | PotatoUrl,
89 | params={"limit": limit, "skip": skip},
90 | expected_length=get_expected_length(limit, skip),
91 | )
92 |
93 | for item in data:
94 | assert item["id"] not in ids
95 | ids.add(item["id"])
96 |
97 | skip += limit
98 |
99 | assert len(ids) == INSERT_COUNT
100 |
101 | @pytest.mark.parametrize("limit", [2, 5, 10])
102 | def test_paging_no_limit(self, client, limit):
103 | client.delete(CarrotUrl)
104 | for i in range(limit):
105 | res = client.post(url=CarrotUrl, json=basic_carrot)
106 | assert res.status_code == 200, res.json()
107 |
108 | res = client.get(CarrotUrl)
109 | assert res.status_code == 200, res.json()
110 | assert len(res.json()) == limit
111 |
112 | res = client.get(CarrotUrl, params={"limit": limit})
113 | assert res.status_code == 200, res.json()
114 | assert len(res.json()) == limit
115 |
116 | limit = int(limit / 2)
117 | res = client.get(CarrotUrl, params={"limit": limit})
118 | assert res.status_code == 200, res.json()
119 | assert len(res.json()) == limit
120 |
--------------------------------------------------------------------------------
/tests/test_pks.py:
--------------------------------------------------------------------------------
1 | from . import test_router
2 |
3 | potato_type = dict(name="russet", origin="Canada")
4 | URL = "/potato_type"
5 |
6 |
7 | def test_get(string_pk_client):
8 | test_router.test_get(string_pk_client, URL)
9 |
10 |
11 | def test_post(string_pk_client):
12 | test_router.test_post(string_pk_client, URL, potato_type)
13 |
14 |
15 | def test_get_one(string_pk_client):
16 | test_router.test_get_one(
17 | string_pk_client, URL, dict(name="kenebec", origin="Ireland"), "name"
18 | )
19 |
20 |
21 | def test_delete_one(string_pk_client):
22 | test_router.test_delete_one(
23 | string_pk_client, URL, dict(name="golden", origin="Ireland"), "name"
24 | )
25 |
26 |
27 | def test_delete_all(string_pk_client):
28 | test_router.test_delete_all(
29 | string_pk_client,
30 | URL,
31 | dict(name="red", origin="Ireland"),
32 | dict(name="brown", origin="Ireland"),
33 | )
34 |
--------------------------------------------------------------------------------
/tests/test_prefix.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from tests.implementations import implementations
4 |
5 |
6 | @pytest.fixture(params=implementations)
7 | def router(request):
8 | impl, dsn = request.param
9 |
10 | app, router, settings = impl(db_uri=dsn)
11 | kwargs = {**settings[0], **dict(prefix=None)}
12 | router = router(**kwargs)
13 |
14 | yield router
15 |
16 |
17 | def test_prefix_lowercase(router):
18 | assert type(router.prefix) is str
19 | assert router.prefix != ""
20 | assert router.prefix == router.prefix.lower()
21 |
--------------------------------------------------------------------------------
/tests/test_router.py:
--------------------------------------------------------------------------------
1 | from typing import Dict
2 |
3 | import pytest
4 |
5 | from .utils import compare_dict
6 |
7 | basic_potato = dict(thickness=0.24, mass=1.2, color="Brown", type="Russet")
8 | URL = "/potato"
9 |
10 |
11 | def test_get(client, url: str = URL, params: dict = None, expected_length: int = 0):
12 | res = client.get(url, params=params)
13 | data = res.json()
14 |
15 | assert res.status_code == 200, data
16 | assert type(data) == list and len(data) == expected_length
17 |
18 | return data
19 |
20 |
21 | def test_post(
22 | client, url: str = URL, model: Dict = None, expected_length: int = 1
23 | ) -> dict:
24 | model = model or basic_potato
25 | res = client.post(url, json=model)
26 | assert res.status_code == 200, res.json()
27 |
28 | data = client.get(url).json()
29 | assert len(data) == expected_length
30 |
31 | return res.json()
32 |
33 |
34 | def test_get_one(client, url: str = URL, model: Dict = None, id_key: str = "id"):
35 | model = model or basic_potato
36 | res = client.post(url, json=model)
37 | assert res.status_code == 200
38 | id_ = res.json()[id_key]
39 |
40 | data = client.get(url).json()
41 | assert len(data)
42 |
43 | res = client.get(f"{url}/{id_}")
44 | assert res.status_code == 200
45 |
46 | assert compare_dict(res.json(), model, exclude=[id_key])
47 |
48 |
49 | def test_update(client, url: str = URL, model: Dict = None, id_key: str = "id"):
50 | test_get(client, url, expected_length=0)
51 |
52 | model = model or basic_potato
53 | res = client.post(url, json=model)
54 | data = res.json()
55 | assert res.status_code == 200
56 |
57 | test_get(client, url, expected_length=1)
58 |
59 | tuber = {k: v for k, v in model.items()}
60 | tuber["color"] = "yellow"
61 |
62 | res = client.put(f"{url}/{data[id_key]}", json=tuber)
63 | assert res.status_code == 200
64 | assert compare_dict(res.json(), tuber, exclude=[id_key])
65 | assert not compare_dict(res.json(), model, exclude=[id_key])
66 |
67 | res = client.get(f"{url}/{data[id_key]}")
68 | assert res.status_code == 200
69 | assert compare_dict(res.json(), tuber, exclude=[id_key])
70 | assert not compare_dict(res.json(), model, exclude=[id_key])
71 |
72 |
73 | def test_delete_one(client, url: str = URL, model: Dict = None, id_key: str = "id"):
74 | model = model or basic_potato
75 | res = client.post(url, json=model)
76 | data = res.json()
77 | assert res.status_code == 200
78 |
79 | res = client.get(f"{url}/{data[id_key]}")
80 | assert res.status_code == 200
81 | assert compare_dict(res.json(), model, exclude=[id_key])
82 |
83 | length_before = len(client.get(url).json())
84 |
85 | res = client.delete(f"{url}/{data[id_key]}")
86 | assert res.status_code == 200
87 | assert compare_dict(res.json(), model, exclude=[id_key])
88 |
89 | res = client.get(url)
90 | assert res.status_code == 200
91 | assert len(res.json()) < length_before
92 |
93 |
94 | def test_delete_all(
95 | client,
96 | url: str = URL,
97 | model: Dict = None,
98 | model2: Dict = None,
99 | ):
100 | model = model or basic_potato
101 | model2 = model2 or basic_potato
102 |
103 | res = client.post(url, json=model)
104 | assert res.status_code == 200
105 |
106 | res = client.post(url, json=model2)
107 | assert res.status_code == 200
108 |
109 | assert len(client.get(url).json()) >= 2
110 |
111 | res = client.delete(url)
112 | assert res.status_code == 200
113 | assert len(res.json()) == 0
114 |
115 | assert len(client.get(url).json()) == 0
116 |
117 |
118 | @pytest.mark.parametrize("id_", [-1, 0, 4, "14"])
119 | def test_not_found(client, id_, url: str = URL, model: Dict = None):
120 | url = f"{url}/{id_}"
121 | model = model or basic_potato
122 | assert client.get(url).status_code == 404
123 | assert client.put(url, json=model).status_code == 404
124 | assert client.delete(url).status_code == 404
125 |
126 |
127 | def test_dne(client):
128 | res = client.get("/")
129 | assert res.status_code == 404
130 |
131 | res = client.get(f"/tomatoes")
132 | assert res.status_code == 404
133 |
--------------------------------------------------------------------------------
/tests/test_sqlalchemy_nested_.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 | from fastapi.testclient import TestClient
4 | from pydantic import BaseModel
5 | from sqlalchemy import Column, ForeignKey, Integer
6 | from sqlalchemy.orm import relationship
7 |
8 | from fastapi_crudrouter import SQLAlchemyCRUDRouter
9 | from tests import ORMModel, test_router
10 | from tests.implementations.sqlalchemy_ import _setup_base_app
11 |
12 | CHILD_URL = "/child"
13 | PARENT_URL = "/parent"
14 |
15 |
16 | class ChildSchema(ORMModel):
17 | parent_id: int
18 |
19 |
20 | class ParentSchema(ORMModel):
21 | children: List[ChildSchema] = []
22 |
23 |
24 | class ParentCreate(BaseModel):
25 | pass
26 |
27 |
28 | def create_app():
29 | app, engine, Base, session = _setup_base_app()
30 |
31 | class Child(Base):
32 | __tablename__ = "child"
33 | id = Column(Integer, primary_key=True, index=True)
34 | parent_id = Column(Integer, ForeignKey("parent.id"))
35 |
36 | class Parent(Base):
37 | __tablename__ = "parent"
38 | id = Column(Integer, primary_key=True, index=True)
39 |
40 | children = relationship(Child, backref="parent", lazy="joined")
41 |
42 | Base.metadata.create_all(bind=engine)
43 | parent_router = SQLAlchemyCRUDRouter(
44 | schema=ParentSchema,
45 | create_schema=ParentCreate,
46 | db_model=Parent,
47 | db=session,
48 | prefix=PARENT_URL,
49 | )
50 | child_router = SQLAlchemyCRUDRouter(
51 | schema=ChildSchema, db_model=Child, db=session, prefix=CHILD_URL
52 | )
53 | app.include_router(parent_router)
54 | app.include_router(child_router)
55 |
56 | return app
57 |
58 |
59 | def test_nested_models():
60 | client = TestClient(create_app())
61 |
62 | parent = test_router.test_post(client, PARENT_URL, dict())
63 | test_router.test_post(client, CHILD_URL, dict(id=0, parent_id=parent["id"]))
64 |
65 | res = client.get(f'{PARENT_URL}/{parent["id"]}')
66 | assert res.status_code == 200, res.json()
67 |
68 | data = res.json()
69 | assert type(data["children"]) is list and data["children"], data
70 |
--------------------------------------------------------------------------------
/tests/test_two_routers.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from . import test_router
4 | from .utils import compare_dict
5 |
6 | basic_potato = dict(thickness=0.24, mass=1.2, color="Brown", type="Russet")
7 | basic_carrot = dict(length=1.2, color="Orange")
8 |
9 | PotatoUrl = "/potato"
10 | CarrotUrl = "/carrot"
11 |
12 |
13 | def test_get(client):
14 | test_router.test_get(client, PotatoUrl)
15 | test_router.test_get(client, CarrotUrl)
16 |
17 |
18 | def test_post(client):
19 | test_router.test_post(client, PotatoUrl, basic_potato)
20 | test_router.test_post(client, CarrotUrl, basic_carrot)
21 |
22 |
23 | def test_get_one(client):
24 | test_router.test_get_one(client, PotatoUrl, basic_potato)
25 | test_router.test_get_one(client, CarrotUrl, basic_carrot)
26 |
27 |
28 | def test_update(client):
29 | test_router.test_update(client, PotatoUrl, basic_potato)
30 |
31 | with pytest.raises(AssertionError):
32 | test_router.test_update(client, CarrotUrl, basic_carrot)
33 |
34 | res = client.post(CarrotUrl, json=basic_carrot)
35 | data = res.json()
36 | assert res.status_code == 200
37 |
38 | carrot = {k: v for k, v in basic_carrot.items()}
39 | carrot["color"] = "Red"
40 | carrot["length"] = 54.0
41 |
42 | res = client.put(f'{CarrotUrl}/{data["id"]}', json=carrot)
43 | assert res.status_code == 200
44 | assert not compare_dict(res.json(), carrot, exclude=["id"])
45 | assert not compare_dict(res.json(), basic_carrot, exclude=["id"])
46 | assert compare_dict(res.json(), carrot, exclude=["id", "color"])
47 |
48 | res = client.get(f'{CarrotUrl}/{data["id"]}')
49 | assert res.status_code == 200
50 | assert not compare_dict(res.json(), carrot, exclude=["id"])
51 | assert not compare_dict(res.json(), basic_carrot, exclude=["id"])
52 | assert compare_dict(res.json(), carrot, exclude=["id", "color"])
53 |
54 |
55 | def test_delete_one(client):
56 | test_router.test_delete_one(client, PotatoUrl, basic_potato)
57 | test_router.test_delete_one(client, CarrotUrl, basic_carrot)
58 |
59 |
60 | def test_delete_all(client):
61 | test_router.test_delete_all(client, PotatoUrl, basic_potato)
62 | test_router.test_delete_all(client, CarrotUrl, basic_carrot, basic_carrot)
63 |
64 |
65 | @pytest.mark.parametrize("id_", [-1, 0, 4, "14"])
66 | def test_not_found(client, id_):
67 | test_router.test_not_found(client, id_, PotatoUrl, basic_potato)
68 | test_router.test_not_found(client, id_, CarrotUrl, basic_carrot)
69 |
--------------------------------------------------------------------------------
/tests/test_version.py:
--------------------------------------------------------------------------------
1 | def test_version():
2 | import fastapi_crudrouter
3 |
4 | assert type(fastapi_crudrouter.__version__) is str
5 |
6 |
7 | def test_version_file():
8 | from fastapi_crudrouter import _version
9 |
10 | assert type(_version.__version__) is str
11 |
--------------------------------------------------------------------------------
/tests/utils.py:
--------------------------------------------------------------------------------
1 | def compare_dict(d1, d2, exclude: list) -> bool:
2 | exclude = exclude or ["id"]
3 | d1 = {k: v for k, v in d1.items() if k not in exclude}
4 | d2 = {k: v for k, v in d2.items() if k not in exclude}
5 | return d1 == d2
6 |
--------------------------------------------------------------------------------