├── .github
├── dependabot.yml
└── workflows
│ ├── deploy-docs.yml
│ ├── main.yml
│ └── publish.yml
├── .gitignore
├── .pre-commit-config.yaml
├── LICENSE
├── README.md
├── docs
├── commands.md
├── generators
│ ├── component.md
│ ├── index.md
│ └── project.md
└── index.md
├── fastapix
├── __init__.py
├── __main__.py
├── commands
│ ├── __init__.py
│ ├── environment.py
│ ├── middlewares.py
│ └── routes.py
├── context.py
├── inference
│ ├── __init__.py
│ ├── analyzers.py
│ └── main.py
├── loader.py
├── main.py
└── py.typed
├── mkdocs.yml
├── pyproject.toml
├── requirements.txt
├── setup.py
└── tests
├── __init__.py
├── commands
├── __init__.py
├── test_env.py
├── test_middlewares.py
└── test_routes.py
├── test_analyzers.py
└── test_version.py
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "pip"
4 | directory: "/"
5 | schedule:
6 | interval: "monthly"
7 |
--------------------------------------------------------------------------------
/.github/workflows/deploy-docs.yml:
--------------------------------------------------------------------------------
1 | name: Deploy Docs via GitHub Pages
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | deploy:
10 | runs-on: ubuntu-latest
11 | if: github.event.repository.fork == false
12 | steps:
13 | - uses: actions/checkout@v3
14 | - uses: actions/setup-python@v4
15 | with:
16 | python-version: "3.10"
17 | - run: pip install mkdocs-material
18 | - run: mkdocs gh-deploy --force
19 |
20 | env:
21 | GH_TOKEN: ${{ secrets.GH_TOKEN }}
22 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: ["main"]
6 | pull_request:
7 | branches: ["main"]
8 |
9 | jobs:
10 | tests:
11 | name: "Python ${{ matrix.python-version }}"
12 | runs-on: ubuntu-latest
13 |
14 | timeout-minutes: 30
15 | strategy:
16 | matrix:
17 | python-version: ["3.7", "3.8", "3.9", "3.10"]
18 |
19 | steps:
20 | - uses: actions/checkout@v3
21 | - uses: actions/setup-python@v4
22 | with:
23 | python-version: ${{ matrix.python-version }}
24 |
25 | - name: Run pre-commit
26 | if: ${{ matrix.python-version == '3.10' }}
27 | uses: pre-commit/action@v3.0.0
28 |
29 | - uses: actions/cache@v3
30 | with:
31 | path: ~/.cache/pip
32 | key: pip-${{ matrix.python-version }}
33 |
34 | - name: Install dependencies
35 | run: |
36 | python -m pip install --upgrade pip setuptools wheel
37 | python -m pip install -r requirements.txt
38 |
39 |
40 | - name: Run tests
41 | run: python -m coverage run --branch --parallel-mode -m pytest
42 |
43 | - name: Upload coverage data
44 | uses: actions/upload-artifact@v3
45 | with:
46 | name: coverage-data
47 | path: '.coverage.*'
48 |
49 | coverage:
50 | name: Coverage
51 | runs-on: ubuntu-20.04
52 | needs: tests
53 | steps:
54 | - uses: actions/checkout@v3
55 |
56 | - uses: actions/setup-python@v3
57 | with:
58 | python-version: '3.10'
59 |
60 | - name: Install dependencies
61 | run: python -m pip install --upgrade coverage[toml]
62 |
63 | - name: Download data
64 | uses: actions/download-artifact@v3
65 | with:
66 | name: coverage-data
67 |
68 | - name: Combine coverage and fail if it's <100%
69 | run: |
70 | python -m coverage combine
71 | python -m coverage html --skip-covered --skip-empty
72 | python -m coverage report --fail-under=100
73 |
74 | - name: Upload HTML report
75 | if: ${{ failure() }}
76 | uses: actions/upload-artifact@v3
77 | with:
78 | name: html-report
79 | path: htmlcov
80 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | version:
7 | description: "Version to be released."
8 | required: true
9 |
10 | jobs:
11 | release:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v3
15 | - uses: actions/setup-python@v4
16 | with:
17 | python-version: "3.10"
18 |
19 | - name: Bump to version ${{ github.event.inputs.version }}
20 | run: |
21 | version='${{ github.event.inputs.version }}'
22 | sed -i -E "s/^__version__ = \"(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)\"/__version__ = \"$version\"/" fastapix/main.py
23 | echo "version=$version" >> $GITHUB_ENV
24 |
25 | - name: Commit version bump
26 | run: |
27 | git config --global user.name 'Github Actions'
28 | git config --global user.email '41898282+github-actions[bot]@users.noreply.github.com'
29 | git add fastapix/main.py
30 | git commit -m "🔖 Release $version"
31 | git push origin main
32 |
33 | - name: Install dependencies
34 | run: python -m pip install --upgrade build twine
35 |
36 | - name: PyPI release
37 | run: |
38 | python -m build
39 | twine upload dist/*
40 | env:
41 | TWINE_USERNAME: __token__
42 | TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
43 |
44 | - name: Create a GitHub Release
45 | run: gh release create v$version --generate-notes
46 | env:
47 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
48 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
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 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
98 | __pypackages__/
99 |
100 | # Celery stuff
101 | celerybeat-schedule
102 | celerybeat.pid
103 |
104 | # SageMath parsed files
105 | *.sage.py
106 |
107 | # Environments
108 | .env
109 | .venv
110 | env/
111 | venv/
112 | ENV/
113 | env.bak/
114 | venv.bak/
115 |
116 | # Spyder project settings
117 | .spyderproject
118 | .spyproject
119 |
120 | # Rope project settings
121 | .ropeproject
122 |
123 | # mkdocs documentation
124 | /site
125 |
126 | # mypy
127 | .mypy_cache/
128 | .dmypy.json
129 | dmypy.json
130 |
131 | # Pyre type checker
132 | .pyre/
133 |
134 | # pytype static type analyzer
135 | .pytype/
136 |
137 | # Cython debug symbols
138 | cython_debug/
139 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/pre-commit/pre-commit-hooks
3 | rev: v4.2.0
4 | hooks:
5 | - id: check-toml
6 | - id: check-yaml
7 | - id: check-json
8 | - id: check-added-large-files
9 | - id: debug-statements
10 | - id: end-of-file-fixer
11 | - id: trailing-whitespace
12 |
13 | - repo: https://github.com/psf/black
14 | rev: 22.3.0
15 | hooks:
16 | - id: black
17 |
18 | - repo: https://github.com/pycqa/isort
19 | rev: 5.10.1
20 | hooks:
21 | - id: isort
22 |
23 | - repo: https://github.com/PyCQA/flake8
24 | rev: 4.0.1
25 | hooks:
26 | - id: flake8
27 | entry: pflake8
28 | additional_dependencies:
29 | - flake8-comprehensions
30 | - pyproject-flake8
31 |
32 | - repo: https://github.com/pre-commit/mirrors-mypy
33 | rev: v0.981
34 | hooks:
35 | - id: mypy
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Marcelo Trylesinski
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 | FastAPI X
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | :warning: **This project is still under development.**
21 |
22 | ## Features
23 |
24 | The most important feature are the project and component generators.
25 |
26 | - [X] Show environment variables the project is reading - `fastapix env`.
27 | - [X] Show application routes. - `fastapix routes`.
28 | - [X] Show application middlewares. - `fastapix middlewares`.
29 | - [ ] Project generator - `fastapix new app`.
30 | - [ ] Generate a new project.
31 | - [ ] Generate Dockerfile.
32 | - [ ] Component generator - `fastapix new component`.
33 | - [ ] Generate a new component based on the current project structure.
34 | - [ ] Plugin system. People will be able to create their own commands.
35 | - [ ] Embed Flake8-FastAPI - `fastapix lint`.
36 | - [ ] Embed formatter - `fastapix format`.
37 | - [ ] Show information about the project - `fastapix info`.
38 |
39 | ## Installation
40 |
41 | ```bash
42 | pip install fastapix
43 | ```
44 |
45 | ## License
46 |
47 | This project is licensed under the terms of the MIT license.
48 |
--------------------------------------------------------------------------------
/docs/commands.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kludex/fastapix/3ec58527e9d17f39a671b94cb3483367ed7421ba/docs/commands.md
--------------------------------------------------------------------------------
/docs/generators/component.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kludex/fastapix/3ec58527e9d17f39a671b94cb3483367ed7421ba/docs/generators/component.md
--------------------------------------------------------------------------------
/docs/generators/index.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kludex/fastapix/3ec58527e9d17f39a671b94cb3483367ed7421ba/docs/generators/index.md
--------------------------------------------------------------------------------
/docs/generators/project.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kludex/fastapix/3ec58527e9d17f39a671b94cb3483367ed7421ba/docs/generators/project.md
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | **Documentation**: View it on [website](https://kludex.github.io/fastapix/)
4 |
5 | **Source Code**: View it on [Github](https://github.com/kludex/fastapix/)
6 |
7 | ---
8 |
9 | **FastAPI X** is a CLI tool to manage your FastAPI projects.
10 |
11 | ## Features 🚀
12 |
13 | - **Generate a new FastAPI project**
14 | - **Generate components to your project**
15 |
16 | ## Installation ✍️
17 |
18 | **FastAPI X** can be installed by running `pip install fastapix`.
19 |
20 | ## Usage 📖
21 |
22 | **FastAPI X** is a CLI tool, so you can run it by typing `fastapix` in your terminal.
23 |
--------------------------------------------------------------------------------
/fastapix/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kludex/fastapix/3ec58527e9d17f39a671b94cb3483367ed7421ba/fastapix/__init__.py
--------------------------------------------------------------------------------
/fastapix/__main__.py:
--------------------------------------------------------------------------------
1 | from fastapix.main import app # pragma: no cover
2 |
3 | if __name__ == "__main__": # pragma: no cover
4 | app()
5 |
--------------------------------------------------------------------------------
/fastapix/commands/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kludex/fastapix/3ec58527e9d17f39a671b94cb3483367ed7421ba/fastapix/commands/__init__.py
--------------------------------------------------------------------------------
/fastapix/commands/environment.py:
--------------------------------------------------------------------------------
1 | """
2 | Command that prints out the environment variables that are used.
3 | """
4 | import io
5 | import json
6 | from inspect import getmembers, isclass
7 | from pathlib import Path
8 | from typing import Type
9 |
10 | import libcst as cst
11 | import libcst.matchers as m
12 | import typer
13 | from pydantic import BaseSettings
14 | from pytablewriter.style import Style
15 | from pytablewriter.writer import MarkdownTableWriter
16 |
17 | from fastapix.context import Context
18 | from fastapix.loader import import_from_filename
19 |
20 | app = typer.Typer(name="env", help="Print out the environment variables used.")
21 |
22 |
23 | # TODO: Add template option.
24 | # TODO: Use rich if output is stdout.
25 | @app.callback(invoke_without_command=True) # type: ignore[misc]
26 | def main(
27 | ctx: Context,
28 | output: Path = typer.Option(Path("/dev/stdout"), help="Output filename."),
29 | ) -> None:
30 | """Print out the environment variables used."""
31 | if ctx.obj.structure.settings is None: # pragma: no cover
32 | ctx.obj.console.print("No settings found.")
33 | raise typer.Exit(2)
34 |
35 | module = import_from_filename(ctx.obj.structure.settings.filename)
36 |
37 | for name, value in getmembers(module):
38 | if isclass(value) and issubclass(value, BaseSettings) and value != BaseSettings:
39 | show_environment(settings_class=value, output=output)
40 |
41 |
42 | def show_environment(settings_class: Type[BaseSettings], output: Path) -> None:
43 | table = []
44 |
45 | schema_json = json.loads(settings_class.schema_json())
46 | properties = schema_json["properties"]
47 | required = schema_json.get("required", [])
48 |
49 | for key, value in properties.items():
50 | required_column = "✅" if key in required else "❌"
51 | table.append(
52 | [
53 | key,
54 | value.get("description", "-"),
55 | value["type"],
56 | str(value.get("default", "-")),
57 | required_column,
58 | ]
59 | )
60 |
61 | writer = MarkdownTableWriter(
62 | headers=["Name", "Description", "Type", "Default", "Required"],
63 | value_matrix=table,
64 | margin=1,
65 | )
66 | writer.set_style(2, Style(align="center"))
67 | writer.set_style(3, Style(align="center"))
68 | writer.set_style(4, Style(align="center"))
69 | writer.stream = io.StringIO()
70 | writer.write_table()
71 |
72 | with output.open("w") as fp:
73 | fp.write(writer.stream.getvalue())
74 |
75 |
76 | # TODO: Use CST instead of loading the module.
77 | class BaseSettingsVisitor(cst.CSTVisitor): # type: ignore[misc] # pragma: no cover
78 | def __init__(self) -> None:
79 | self.inside_settings = False
80 |
81 | def visit_ClassDef(self, node: cst.ClassDef) -> None:
82 | for base in node.bases:
83 | if m.matches(base, m.Arg(value=m.Name("BaseSettings"))):
84 | self.inside_settings = True
85 |
86 | def leave_ClassDef(self, original_node: cst.ClassDef) -> None:
87 | self.inside_settings = False
88 |
--------------------------------------------------------------------------------
/fastapix/commands/middlewares.py:
--------------------------------------------------------------------------------
1 | from inspect import getmembers
2 | from typing import Union
3 |
4 | import typer
5 | from fastapi import FastAPI
6 | from rich.table import Table
7 |
8 | from fastapix.context import Context
9 | from fastapix.loader import import_from_filename
10 |
11 | app = typer.Typer(name="middlewares", help="List FastAPI middlewares.")
12 |
13 |
14 | @app.callback(invoke_without_command=True) # type: ignore[misc]
15 | def middlewares(ctx: Context) -> None:
16 | if ctx.obj.structure.app is None: # pragma: no cover
17 | ctx.obj.console.print("No app found.")
18 | raise typer.Exit(2)
19 |
20 | module = import_from_filename(ctx.obj.structure.app.filename)
21 | app: Union[FastAPI, None] = None
22 | # TODO: Support factory functions.
23 | for name, value in getmembers(module):
24 | if isinstance(value, FastAPI):
25 | app = value
26 |
27 | # TODO: PR welcome to add test.
28 | if app is None: # pragma: no cover
29 | raise RuntimeError("Could not find FastAPI instance.")
30 |
31 | headers = ("middleware", "parameter", "value")
32 | middlewares = [
33 | (
34 | middleware.cls.__name__,
35 | "\n".join(str(key) for key in middleware.options.keys()),
36 | "\n".join(str(value) for value in middleware.options.values()),
37 | )
38 | for middleware in app.user_middleware
39 | ]
40 | for middleware in app.user_middleware:
41 | print(middleware.cls, middleware.options)
42 | table = Table(show_header=True, header_style="bold magenta")
43 | for column in headers:
44 | table.add_column(column)
45 | for middleware in middlewares:
46 | table.add_row(*middleware)
47 | ctx.obj.console.print(table)
48 |
--------------------------------------------------------------------------------
/fastapix/commands/routes.py:
--------------------------------------------------------------------------------
1 | from inspect import getmembers
2 | from typing import Union
3 |
4 | import typer
5 | from fastapi import FastAPI
6 | from rich.table import Table
7 |
8 | from fastapix.context import Context
9 | from fastapix.loader import import_from_filename
10 |
11 | app = typer.Typer(name="routes", help="List FastAPI routes.")
12 |
13 |
14 | @app.callback(invoke_without_command=True) # type: ignore[misc]
15 | def routes(ctx: Context) -> None:
16 | if ctx.obj.structure.app is None: # pragma: no cover
17 | ctx.obj.console.print("No app found.")
18 | raise typer.Exit(2)
19 |
20 | module = import_from_filename(ctx.obj.structure.app.filename)
21 | app: Union[FastAPI, None] = None
22 | # TODO: Support factory functions.
23 | for name, value in getmembers(module):
24 | if isinstance(value, FastAPI):
25 | app = value
26 |
27 | # TODO: PR welcome to add test.
28 | if app is None: # pragma: no cover
29 | raise RuntimeError("Could not find FastAPI instance.")
30 |
31 | headers = ("name", "path", "methods")
32 | routes: list[tuple[str, str, str]] = []
33 | for route in app.routes:
34 | name = str(getattr(route, "name"))
35 | path = str(getattr(route, "path"))
36 | methods = sorted(getattr(route, "methods", None) or {})
37 | routes.append((name, path, str(methods)))
38 | routes.sort(key=lambda x: x[1])
39 | table = Table(show_header=True, header_style="bold magenta")
40 | for column in headers:
41 | table.add_column(column)
42 | for route in routes:
43 | table.add_row(*route)
44 | ctx.obj.console.print(table)
45 |
--------------------------------------------------------------------------------
/fastapix/context.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 | from typing import Union
3 |
4 | from rich.console import Console
5 | from typer import Context as _Context
6 |
7 |
8 | @dataclass
9 | class Component:
10 | filename: str
11 | content: str
12 |
13 |
14 | @dataclass
15 | class ProjectStructure:
16 | settings: Union[Component, None]
17 | app: Union[Component, None]
18 |
19 |
20 | @dataclass
21 | class ContextObject:
22 | console: Console
23 | structure: ProjectStructure
24 |
25 |
26 | class Context(_Context): # type: ignore[misc]
27 | obj: ContextObject
28 |
--------------------------------------------------------------------------------
/fastapix/inference/__init__.py:
--------------------------------------------------------------------------------
1 | from fastapix.inference.main import infer_project_structure as infer_project_structure
2 |
--------------------------------------------------------------------------------
/fastapix/inference/analyzers.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 |
4 | class Analyzer:
5 | def match(self, content: str) -> bool:
6 | raise NotImplementedError # pragma: no cover
7 |
8 |
9 | class SettingsAnalyzer(Analyzer):
10 | def match(self, content: str) -> bool:
11 | if re.search(r"class .+\(BaseSettings\):", content):
12 | return True
13 | return False
14 |
15 |
16 | class ApplicationAnalyzer(Analyzer):
17 | def match(self, content: str) -> bool:
18 | """Check if the content contains a FastAPI instance.
19 |
20 | This is a very naive implementation, and it's expected to fail.
21 | """
22 | regex = "|".join([r"=\s*FastAPI\(\)", r"return FastAPI\("])
23 | if re.search(regex, content):
24 | return True
25 | return False
26 |
--------------------------------------------------------------------------------
/fastapix/inference/main.py:
--------------------------------------------------------------------------------
1 | import glob
2 | import os
3 | from typing import Union
4 |
5 | from rich.console import Console
6 |
7 | from fastapix.context import Component, ProjectStructure
8 | from fastapix.inference.analyzers import ApplicationAnalyzer, SettingsAnalyzer
9 |
10 | # TODO: Limit the number of files. If surpass the threshold, ask for manual input.
11 |
12 |
13 | def infer_project_structure(console: Console) -> ProjectStructure:
14 | python_pattern = os.getcwd() + "/**/*.py"
15 |
16 | settings_analyzer = SettingsAnalyzer()
17 | settings: Union[Component, None] = None
18 |
19 | app_analyzer = ApplicationAnalyzer()
20 | app: Union[Component, None] = None
21 |
22 | filenames = iter(glob.glob(python_pattern, recursive=True))
23 | filename = next(filenames, None)
24 | while filename and (app is None or settings is None):
25 | with open(filename, "r") as f:
26 | content = f.read()
27 | if settings_analyzer.match(content): # pragma: no cover
28 | settings = Component(filename=filename, content=content)
29 | if app_analyzer.match(content): # pragma: no cover
30 | app = Component(filename=filename, content=content)
31 | filename = next(filenames, None)
32 |
33 | return ProjectStructure(settings=settings, app=app)
34 |
--------------------------------------------------------------------------------
/fastapix/loader.py:
--------------------------------------------------------------------------------
1 | import importlib.util
2 | import sys
3 | from types import ModuleType
4 |
5 |
6 | def import_from_filename(filename: str) -> ModuleType:
7 | """Import a module from a filename."""
8 | spec = importlib.util.spec_from_file_location("-", filename)
9 | if spec is None or spec.loader is None:
10 | raise RuntimeError("Could not load settings file.") # pragma: no cover
11 |
12 | module = importlib.util.module_from_spec(spec)
13 | sys.modules["module"] = module
14 | spec.loader.exec_module(module)
15 |
16 | return module
17 |
--------------------------------------------------------------------------------
/fastapix/main.py:
--------------------------------------------------------------------------------
1 | import platform
2 | from pathlib import Path
3 |
4 | import typer
5 | from appdirs import user_cache_dir
6 | from rich.console import Console
7 |
8 | from fastapix.commands.environment import app as env_app
9 | from fastapix.commands.middlewares import app as middlewares_app
10 | from fastapix.commands.routes import app as routes_app
11 | from fastapix.context import Context, ContextObject
12 | from fastapix.inference import infer_project_structure
13 |
14 | __version__ = "0.1.0"
15 |
16 |
17 | app = typer.Typer(name="FastAPI X", help="Manage your FastAPI project.")
18 | app.add_typer(env_app)
19 | app.add_typer(routes_app)
20 | app.add_typer(middlewares_app)
21 |
22 |
23 | def version_callback(value: bool) -> None:
24 | if value:
25 | typer.echo(
26 | "Running FastAPI X {} with {} {} on {}.".format(
27 | __version__,
28 | platform.python_implementation(),
29 | platform.python_version(),
30 | platform.system(),
31 | )
32 | )
33 | raise typer.Exit(0)
34 |
35 |
36 | @app.callback() # type: ignore[misc]
37 | def main(
38 | ctx: Context,
39 | version: bool = typer.Option(
40 | None,
41 | "--version",
42 | callback=version_callback,
43 | is_eager=True,
44 | help="Show version and exit.",
45 | ),
46 | ) -> None:
47 | console = Console()
48 |
49 | cache_dir = Path(user_cache_dir("fastapix"))
50 | cache_dir.mkdir(exist_ok=True)
51 |
52 | # TODO: Create an entry per project. Each entry that represents a file should have
53 | # a hash.
54 |
55 | project_structure = infer_project_structure(console)
56 | ctx.obj = ContextObject(console=console, structure=project_structure)
57 |
--------------------------------------------------------------------------------
/fastapix/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kludex/fastapix/3ec58527e9d17f39a671b94cb3483367ed7421ba/fastapix/py.typed
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: FastAPI X
2 | theme:
3 | name: material
4 | palette:
5 | primary: "blue"
6 | accent: "orange"
7 | font:
8 | text: "Ubuntu"
9 | repo_name: Kludex/fastapix
10 | repo_url: https://github.com/Kludex/fastapix
11 |
12 | nav:
13 | - FastAPI X: index.md
14 | - Commands: commands.md
15 | - Generators:
16 | - Generators: generators/index.md
17 | - Project: generators/project.md
18 | - Component: generators/component.md
19 |
20 | markdown_extensions:
21 | - toc:
22 | permalink: true
23 | - markdown.extensions.codehilite:
24 | guess_lang: false
25 | - admonition
26 | - codehilite
27 | - extra
28 | - tables
29 | - smarty
30 | - pymdownx.tabbed
31 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["hatchling"]
3 | build-backend = 'hatchling.build'
4 |
5 | [project]
6 | name = "fastapix"
7 | description = "FastAPI X is a powerful CLI tool to help you! :rocket:"
8 | readme = "README.md"
9 | authors = [{ name = "Marcelo Trylesinski", email = "marcelotryle@email.com" }]
10 | classifiers = [
11 | "Development Status :: 3 - Alpha",
12 | "License :: OSI Approved :: MIT License",
13 | "Intended Audience :: Developers",
14 | "Natural Language :: English",
15 | "Operating System :: OS Independent",
16 | "Programming Language :: Python :: 3 :: Only",
17 | "Programming Language :: Python :: 3",
18 | "Programming Language :: Python :: 3.7",
19 | "Programming Language :: Python :: 3.8",
20 | "Programming Language :: Python :: 3.9",
21 | "Programming Language :: Python :: 3.10",
22 | ]
23 | license = "MIT"
24 | requires-python = ">=3.7"
25 | dependencies = [
26 | "typer>=0.6.1",
27 | "rich>=12.6.0",
28 | "appdirs>=1.4.4",
29 | "libcst>=0.4.7",
30 | "pytablewriter>=0.64.2",
31 | ]
32 | optional-dependencies = {}
33 | dynamic = ["version"]
34 |
35 | [tool.hatch.version]
36 | path = "fastapix/main.py"
37 |
38 | [project.urls]
39 | Homepage = "https://github.com/Kludex/fastapix"
40 | Source = "https://github.com/Kludex/fastapix"
41 | Twitter = "https://twitter.com/marcelotryle"
42 | Funding = 'https://github.com/sponsors/Kludex'
43 |
44 | [project.scripts]
45 | fastapix = "fastapix.main:app"
46 |
47 | [tool.mypy]
48 | strict = true
49 | show_error_codes = true
50 |
51 | [tool.flake8]
52 | statistics = true
53 | max-line-length = 88
54 | ignore = ["E203", "E501", "W503"]
55 | per-file-ignores = ["__init__.py:F401"]
56 |
57 | [tool.black]
58 | target-version = ["py37"]
59 |
60 | [tool.isort]
61 | profile = "black"
62 | combine_as_imports = true
63 |
64 | [tool.pytest.ini_options]
65 | addopts = ["--strict-config", "--strict-markers"]
66 | filterwarnings = ["error"]
67 |
68 | [tool.coverage.run]
69 | source_pkgs = ["fastapix", "tests"]
70 |
71 | [tool.coverage.report]
72 | show_missing = true
73 | skip_covered = true
74 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | -e .
2 |
3 | # Linter & Formatter
4 | isort
5 | flake8
6 | black
7 | mypy
8 | pyproject-flake8
9 | pre-commit
10 |
11 | # Tests
12 | coverage[toml]
13 | pytest
14 | pytest-sugar
15 | dirty-equals
16 | fastapi
17 |
18 | # Build
19 | hatch
20 |
21 | # Docs
22 | mkdocs-material
23 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | from setuptools import setup
4 |
5 | sys.stderr.write(
6 | """
7 | ===============================
8 | Unsupported installation method
9 | ===============================
10 | fastapix no longer supports installation with `python setup.py install`.
11 | Please use `python -m pip install .` instead.
12 | """
13 | )
14 | sys.exit(1)
15 |
16 |
17 | # The below code will never execute, however GitHub is particularly
18 | # picky about where it finds Python packaging metadata.
19 | # See: https://github.com/github/feedback/discussions/6456
20 | #
21 | # To be removed once GitHub catches up.
22 |
23 | setup(
24 | name="fastapix",
25 | install_requires=[],
26 | )
27 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kludex/fastapix/3ec58527e9d17f39a671b94cb3483367ed7421ba/tests/__init__.py
--------------------------------------------------------------------------------
/tests/commands/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kludex/fastapix/3ec58527e9d17f39a671b94cb3483367ed7421ba/tests/commands/__init__.py
--------------------------------------------------------------------------------
/tests/commands/test_env.py:
--------------------------------------------------------------------------------
1 | import textwrap
2 | from pathlib import Path
3 |
4 | from typer.testing import CliRunner
5 |
6 | from fastapix.main import app
7 |
8 |
9 | def test_env_command(tmp_path: Path) -> None:
10 | content = textwrap.dedent(
11 | """
12 | from pydantic import BaseSettings
13 |
14 | class Settings(BaseSettings):
15 | foo: str = "bar"
16 | bar: int
17 | """
18 | )
19 | runner = CliRunner()
20 | with runner.isolated_filesystem(temp_dir=tmp_path) as td:
21 | file = Path(td) / "settings.py"
22 | file.write_text(content)
23 | output = Path(td) / "output.md"
24 | result = runner.invoke(app, ["env", "--output", str(output)])
25 | assert result.exit_code == 0, result.stdout
26 | with output.open("r") as f:
27 | content = f.read()
28 | expected = (
29 | "| Name | Description | Type | Default | Required |",
30 | "| ---- | ----------- | :-----: | :-----: | :------: |",
31 | "| foo | - | string | bar | ❌ |",
32 | "| bar | - | integer | - | ✅ |",
33 | )
34 | assert all(line in content for line in expected)
35 |
--------------------------------------------------------------------------------
/tests/commands/test_middlewares.py:
--------------------------------------------------------------------------------
1 | import textwrap
2 | from pathlib import Path
3 |
4 | from typer.testing import CliRunner
5 |
6 | from fastapix.main import app
7 |
8 |
9 | def test_middlewares_command(tmp_path: Path) -> None:
10 | content = textwrap.dedent(
11 | """
12 | from fastapi import FastAPI
13 | from fastapi.middleware.cors import CORSMiddleware
14 |
15 | app = FastAPI()
16 | app.add_middleware(
17 | CORSMiddleware,
18 | allow_origins=["*"],
19 | allow_credentials=True,
20 | allow_methods=["*"],
21 | allow_headers=["*"],
22 | )
23 | """
24 | )
25 | runner = CliRunner()
26 | with runner.isolated_filesystem(temp_dir=tmp_path) as td:
27 | file = Path(td) / "main.py"
28 | file.write_text(content)
29 | result = runner.invoke(app, ["middlewares"])
30 | assert result.exit_code == 0, result.stdout
31 | expected = textwrap.dedent(
32 | """
33 | ┏━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━┓
34 | ┃ middleware ┃ parameter ┃ value ┃
35 | ┡━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━┩
36 | │ CORSMiddleware │ allow_origins │ ['*'] │
37 | │ │ allow_credentials │ True │
38 | │ │ allow_methods │ ['*'] │
39 | │ │ allow_headers │ ['*'] │
40 | └────────────────┴───────────────────┴───────┘
41 | """
42 | )
43 | assert expected in result.stdout, result.stdout
44 |
--------------------------------------------------------------------------------
/tests/commands/test_routes.py:
--------------------------------------------------------------------------------
1 | import textwrap
2 | from pathlib import Path
3 |
4 | from typer.testing import CliRunner
5 |
6 | from fastapix.main import app
7 |
8 |
9 | def test_routes_command(tmp_path: Path) -> None:
10 | content = textwrap.dedent(
11 | """
12 | from fastapi import FastAPI
13 |
14 | app = FastAPI()
15 |
16 | @app.get("/")
17 | def index():
18 | return {"message": "Hello World!"}
19 | """
20 | )
21 | runner = CliRunner()
22 | with runner.isolated_filesystem(temp_dir=tmp_path) as td:
23 | file = Path(td) / "main.py"
24 | file.write_text(content)
25 | result = runner.invoke(app, ["routes"])
26 | assert result.exit_code == 0, result.stdout
27 | expected = textwrap.dedent(
28 | """┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓
29 | ┃ name ┃ path ┃ methods ┃
30 | ┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━┩
31 | │ index │ / │ ['GET'] │
32 | │ swagger_ui_html │ /docs │ ['GET', 'HEAD'] │
33 | │ swagger_ui_redirect │ /docs/oauth2-redirect │ ['GET', 'HEAD'] │
34 | │ openapi │ /openapi.json │ ['GET', 'HEAD'] │
35 | │ redoc_html │ /redoc │ ['GET', 'HEAD'] │
36 | └─────────────────────┴───────────────────────┴─────────────────┘
37 | """
38 | )
39 | assert expected in result.stdout
40 |
--------------------------------------------------------------------------------
/tests/test_analyzers.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from fastapix.inference.analyzers import ApplicationAnalyzer, SettingsAnalyzer
4 |
5 |
6 | @pytest.mark.parametrize( # type: ignore[misc]
7 | "content, expected",
8 | [
9 | ("class Settings(BaseSettings):", True),
10 | ("class Potato(Settings):", False),
11 | ],
12 | )
13 | def test_settings_analyzer(content: str, expected: bool) -> None:
14 | analyzer = SettingsAnalyzer()
15 | assert analyzer.match(content) == expected
16 |
17 |
18 | @pytest.mark.parametrize( # type: ignore[misc]
19 | "content, expected",
20 | [
21 | ("app = FastAPI()", True),
22 | ("return FastAPI(", True),
23 | ("def potato() -> FastAPI:", False),
24 | ],
25 | )
26 | def test_application_analyzer(content: str, expected: bool) -> None:
27 | analyzer = ApplicationAnalyzer()
28 | assert analyzer.match(content) == expected
29 |
--------------------------------------------------------------------------------
/tests/test_version.py:
--------------------------------------------------------------------------------
1 | from typer.testing import CliRunner
2 |
3 | from fastapix.main import app
4 |
5 | runner = CliRunner()
6 |
7 |
8 | def test_version() -> None:
9 | result = runner.invoke(app, ["--version"])
10 | assert result.exit_code == 0
11 | assert "FastAPI X" in result.stdout
12 |
--------------------------------------------------------------------------------