diff --git a/.github/DISCUSSION_TEMPLATE/questions.yml b/.github/DISCUSSION_TEMPLATE/questions.yml index ac76bb3..d94ede7 100644 --- a/.github/DISCUSSION_TEMPLATE/questions.yml +++ b/.github/DISCUSSION_TEMPLATE/questions.yml @@ -95,4 +95,4 @@ body: id: context attributes: label: Additional Context - description: Add any additional context information or screenshots you think are useful. + description: Add any additional context information or screenshots you think are useful. \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 0ffc101..08eec0f 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -github: [tiangolo] +github: [tiangolo] \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 206b51e..e94918a 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -7,4 +7,4 @@ contact_links: url: https://github.com/fastapi/fastapi-new/discussions/categories/questions - name: Feature Request about: To suggest an idea or ask about a feature, please start with a question saying what you would like to achieve. There might be a way to do it already. - url: https://github.com/fastapi/fastapi-new/discussions/categories/questions + url: https://github.com/fastapi/fastapi-new/discussions/categories/questions \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/privileged.yml b/.github/ISSUE_TEMPLATE/privileged.yml index 26827d4..90a827e 100644 --- a/.github/ISSUE_TEMPLATE/privileged.yml +++ b/.github/ISSUE_TEMPLATE/privileged.yml @@ -19,4 +19,4 @@ body: id: content attributes: label: Issue Content - description: Add the content of the issue here. + description: Add the content of the issue here. \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml index dd7fda3..f4fa41e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -19,4 +19,4 @@ updates: schedule: interval: "monthly" commit-message: - prefix: โฌ† + prefix: โฌ† \ No newline at end of file diff --git a/.github/labeler.yml b/.github/labeler.yml index 6f5221e..ff5db1a 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,11 +1,11 @@ internal: - all: - - changed-files: - - any-glob-to-any-file: - - .github/** - - scripts/** - - .gitignore - - .pre-commit-config.yaml - - all-globs-to-all-files: - - '!src/**' - - '!pyproject.toml' + - changed-files: + - any-glob-to-any-file: + - .github/** + - scripts/** + - .gitignore + - .pre-commit-config.yaml + - all-globs-to-all-files: + - "!src/**" + - "!pyproject.toml" \ No newline at end of file diff --git a/.github/workflows/add-to-project.yml b/.github/workflows/add-to-project.yml index 8f055fc..e3c7b49 100644 --- a/.github/workflows/add-to-project.yml +++ b/.github/workflows/add-to-project.yml @@ -15,4 +15,4 @@ jobs: - uses: actions/add-to-project@v1.0.2 with: project-url: https://github.com/orgs/fastapi/projects/2 - github-token: ${{ secrets.FASTAPI_PROJECTS_TOKEN }} + github-token: ${{ secrets.FASTAPI_PROJECTS_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/detect-conflicts.yml b/.github/workflows/detect-conflicts.yml index b46c11e..e59b90e 100644 --- a/.github/workflows/detect-conflicts.yml +++ b/.github/workflows/detect-conflicts.yml @@ -17,4 +17,4 @@ jobs: with: dirtyLabel: conflicts repoToken: "${{ secrets.GITHUB_TOKEN }}" - commentOnDirty: This pull request has a merge conflict that needs to be resolved. + commentOnDirty: This pull request has a merge conflict that needs to be resolved. \ No newline at end of file diff --git a/.github/workflows/issue-manager.yml b/.github/workflows/issue-manager.yml index b97ed6a..95d2f7b 100644 --- a/.github/workflows/issue-manager.yml +++ b/.github/workflows/issue-manager.yml @@ -48,4 +48,4 @@ jobs: "delay": 0, "message": "This was marked as invalid and will be closed now. If this is an error, please provide additional details." } - } + } \ No newline at end of file diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 7aeb448..ffc0355 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -16,9 +16,9 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/labeler@v6 - if: ${{ github.event.action != 'labeled' && github.event.action != 'unlabeled' }} - - run: echo "Done adding labels" + - uses: actions/labeler@v6 + if: ${{ github.event.action != 'labeled' && github.event.action != 'unlabeled' }} + - run: echo "Done adding labels" # Run this after labeler applied labels check-labels: needs: @@ -30,4 +30,4 @@ jobs: - uses: docker://agilepathway/pull-request-label-checker:latest with: one_of: breaking,security,feature,bug,refactor,upgrade,docs,lang-all,internal - repo_token: ${{ secrets.GITHUB_TOKEN }} + repo_token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/latest-changes.yml b/.github/workflows/latest-changes.yml index ae22e73..9adda8d 100644 --- a/.github/workflows/latest-changes.yml +++ b/.github/workflows/latest-changes.yml @@ -12,9 +12,9 @@ on: description: PR number required: true debug_enabled: - description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' + description: "Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)" required: false - default: 'false' + default: "false" jobs: latest-changes: @@ -38,7 +38,7 @@ jobs: with: token: ${{ secrets.GITHUB_TOKEN }} latest_changes_file: release-notes.md - latest_changes_header: '## Latest Changes' - end_regex: '^## ' + latest_changes_header: "## Latest Changes" + end_regex: "^## " debug_logs: true - label_header_prefix: '### ' + label_header_prefix: "### " \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 08e76d0..4690f9c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -29,4 +29,4 @@ jobs: - name: Dump GitHub context env: GITHUB_CONTEXT: ${{ toJson(github) }} - run: echo "$GITHUB_CONTEXT" + run: echo "$GITHUB_CONTEXT" \ No newline at end of file diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index c9a7dd7..e2e710b 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -26,7 +26,7 @@ jobs: - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: - python-version: '3.13' + python-version: "3.13" - name: Setup uv uses: astral-sh/setup-uv@v7 with: @@ -59,4 +59,4 @@ jobs: SMOKESHOW_GITHUB_CONTEXT: coverage SMOKESHOW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SMOKESHOW_GITHUB_PR_HEAD_SHA: ${{ github.event.workflow_run.head_sha }} - SMOKESHOW_AUTH_KEY: ${{ secrets.SMOKESHOW_AUTH_KEY }} + SMOKESHOW_AUTH_KEY: ${{ secrets.SMOKESHOW_AUTH_KEY }} \ No newline at end of file diff --git a/.github/workflows/test-redistribute.yml b/.github/workflows/test-redistribute.yml index 9b8278d..c25834d 100644 --- a/.github/workflows/test-redistribute.yml +++ b/.github/workflows/test-redistribute.yml @@ -55,7 +55,7 @@ jobs: pip wheel --no-deps fastapi_new*.tar.gz # https://github.com/marketplace/actions/alls-green#why - test-redistribute-alls-green: # This job does nothing and is only used for the branch protection + test-redistribute-alls-green: # This job does nothing and is only used for the branch protection if: always() needs: - test-redistribute @@ -64,4 +64,4 @@ jobs: - name: Decide whether the needed jobs succeeded or failed uses: re-actors/alls-green@release/v1 with: - jobs: ${{ toJSON(needs) }} + jobs: ${{ toJSON(needs) }} \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e04bbc3..8e959ce 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,9 +11,9 @@ on: workflow_dispatch: inputs: debug_enabled: - description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' + description: "Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)" required: false - default: 'false' + default: "false" schedule: # cron every week on monday - cron: "0 0 * * 1" @@ -89,7 +89,7 @@ jobs: - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: - python-version: '3.13' + python-version: "3.13" - name: Setup uv uses: astral-sh/setup-uv@v7 with: @@ -124,7 +124,7 @@ jobs: include-hidden-files: true # https://github.com/marketplace/actions/alls-green#why - alls-green: # This job does nothing and is only used for the branch protection + alls-green: # This job does nothing and is only used for the branch protection if: always() needs: - coverage-combine @@ -133,4 +133,4 @@ jobs: - name: Decide whether the needed jobs succeeded or failed uses: re-actors/alls-green@release/v1 with: - jobs: ${{ toJSON(needs) }} + jobs: ${{ toJSON(needs) }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4f38e2f..103768d 100644 --- a/.gitignore +++ b/.gitignore @@ -16,17 +16,18 @@ htmlcov/ .coverage.* coverage/ .pytest_cache/ +tox/ # Type checking .mypy_cache/ # Virtual environments .venv +venv/ # IDE -.vscode/ .idea/ # OS .DS_Store -Thumbs.db +Thumbs.db \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9c075f6..1341154 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,25 +1,25 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks default_language_version: - python: python3.10 + python: python3.10 repos: -- repo: https://github.com/pre-commit/pre-commit-hooks + - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - - id: check-added-large-files - - id: check-toml - - id: check-yaml + - id: check-added-large-files + - id: check-toml + - id: check-yaml args: - - --unsafe - - id: end-of-file-fixer - - id: trailing-whitespace -- repo: https://github.com/astral-sh/ruff-pre-commit + - --unsafe + - id: end-of-file-fixer + - id: trailing-whitespace + - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.13.3 hooks: - - id: ruff + - id: ruff args: - - --fix - - id: ruff-format + - --fix + - id: ruff-format ci: - autofix_commit_msg: ๐ŸŽจ [pre-commit.ci] Auto format from pre-commit.com hooks - autoupdate_commit_msg: โฌ† [pre-commit.ci] pre-commit autoupdate + autofix_commit_msg: ๐ŸŽจ [pre-commit.ci] Auto format from pre-commit.com hooks + autoupdate_commit_msg: โฌ† [pre-commit.ci] pre-commit autoupdate \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a653827 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,19 @@ +{ + "editor.tabSize": 4, + "editor.formatOnSave": true, + "files.exclude": { + "**/__pycache__": true, + "**/.pytest_cache": true, + "**/.DS_Store": true, + "**/.hg": true, + "**/.git": true, + "**/*.pyc": true, + "**/.svn": true, + "**/Thumbs.db": true, + "**/venv": true, + "**/.venv": true, + }, + "python.analysis.autoImportCompletions": true, + "python.analysis.completeFunctionParens": true, + "python.analysis.typeCheckingMode": "strict" +} \ No newline at end of file diff --git a/release-notes.md b/CHANGELOG.md similarity index 60% rename from release-notes.md rename to CHANGELOG.md index 7ffb685..6c37258 100644 --- a/release-notes.md +++ b/CHANGELOG.md @@ -4,34 +4,34 @@ ### Internal -* โฌ† Bump actions/checkout from 5 to 6. PR [#16](https://github.com/fastapi/fastapi-new/pull/16) by [@dependabot[bot]](https://github.com/apps/dependabot). -* โฌ† Bump actions/checkout from 5 to 6. PR [#14](https://github.com/fastapi/fastapi-new/pull/14) by [@dependabot[bot]](https://github.com/apps/dependabot). -* ๐Ÿ‘ท Upgrade `latest-changes` GitHub Action and pin `actions/checkout@v5`. PR [#15](https://github.com/fastapi/fastapi-new/pull/15) by [@svlandeg](https://github.com/svlandeg). -* โฌ† Bump ruff from 0.14.1 to 0.14.3. PR [#7](https://github.com/fastapi/fastapi-new/pull/7) by [@dependabot[bot]](https://github.com/apps/dependabot). +- โฌ† Bump actions/checkout from 5 to 6. PR [#16](https://github.com/fastapi/fastapi-new/pull/16) by [@dependabot[bot]](https://github.com/apps/dependabot). +- โฌ† Bump actions/checkout from 5 to 6. PR [#14](https://github.com/fastapi/fastapi-new/pull/14) by [@dependabot[bot]](https://github.com/apps/dependabot). +- ๐Ÿ‘ท Upgrade `latest-changes` GitHub Action and pin `actions/checkout@v5`. PR [#15](https://github.com/fastapi/fastapi-new/pull/15) by [@svlandeg](https://github.com/svlandeg). +- โฌ† Bump ruff from 0.14.1 to 0.14.3. PR [#7](https://github.com/fastapi/fastapi-new/pull/7) by [@dependabot[bot]](https://github.com/apps/dependabot). ## 0.0.2 ### Features -* โœจ Add implementation of `fastapi-new` CLI, and base for `fastapi new` command. PR [#5](https://github.com/fastapi/fastapi-new/pull/5) by [@savannahostrowski](https://github.com/savannahostrowski). +- โœจ Add implementation of `fastapi-new` CLI, and base for `fastapi new` command. PR [#5](https://github.com/fastapi/fastapi-new/pull/5) by [@savannahostrowski](https://github.com/savannahostrowski). ### Refactors -* ๐Ÿ‘ท Switch to dynamic versioning with `pdm` build system. PR [#11](https://github.com/fastapi/fastapi-new/pull/11) by [@tiangolo](https://github.com/tiangolo). +- ๐Ÿ‘ท Switch to dynamic versioning with `pdm` build system. PR [#11](https://github.com/fastapi/fastapi-new/pull/11) by [@tiangolo](https://github.com/tiangolo). ### Docs -* ๐Ÿ“ Add instructions to README. PR [#12](https://github.com/fastapi/fastapi-new/pull/12) by [@tiangolo](https://github.com/tiangolo). +- ๐Ÿ“ Add instructions to README. PR [#12](https://github.com/fastapi/fastapi-new/pull/12) by [@tiangolo](https://github.com/tiangolo). ### Internal -* โฌ† Bump mypy from 1.14.1 to 1.18.2. PR [#10](https://github.com/fastapi/fastapi-new/pull/10) by [@dependabot[bot]](https://github.com/apps/dependabot). -* โฌ† Bump actions/download-artifact from 5 to 6. PR [#6](https://github.com/fastapi/fastapi-new/pull/6) by [@dependabot[bot]](https://github.com/apps/dependabot). -* โฌ† Bump actions/upload-artifact from 4 to 5. PR [#4](https://github.com/fastapi/fastapi-new/pull/4) by [@dependabot[bot]](https://github.com/apps/dependabot). -* ๐Ÿ”ง Tweak tagline for `pyproject.toml`. PR [#2](https://github.com/fastapi/fastapi-new/pull/2) by [@tiangolo](https://github.com/tiangolo). -* ๐Ÿ‘ท Update version of Smokeshow. PR [#3](https://github.com/fastapi/fastapi-new/pull/3) by [@tiangolo](https://github.com/tiangolo). -* ๐Ÿ‘ท Add tests setup for CI. PR [#1](https://github.com/fastapi/fastapi-new/pull/1) by [@tiangolo](https://github.com/tiangolo). +- โฌ† Bump mypy from 1.14.1 to 1.18.2. PR [#10](https://github.com/fastapi/fastapi-new/pull/10) by [@dependabot[bot]](https://github.com/apps/dependabot). +- โฌ† Bump actions/download-artifact from 5 to 6. PR [#6](https://github.com/fastapi/fastapi-new/pull/6) by [@dependabot[bot]](https://github.com/apps/dependabot). +- โฌ† Bump actions/upload-artifact from 4 to 5. PR [#4](https://github.com/fastapi/fastapi-new/pull/4) by [@dependabot[bot]](https://github.com/apps/dependabot). +- ๐Ÿ”ง Tweak tagline for `pyproject.toml`. PR [#2](https://github.com/fastapi/fastapi-new/pull/2) by [@tiangolo](https://github.com/tiangolo). +- ๐Ÿ‘ท Update version of Smokeshow. PR [#3](https://github.com/fastapi/fastapi-new/pull/3) by [@tiangolo](https://github.com/tiangolo). +- ๐Ÿ‘ท Add tests setup for CI. PR [#1](https://github.com/fastapi/fastapi-new/pull/1) by [@tiangolo](https://github.com/tiangolo). ## 0.0.1 -Reserve PyPI package. +Reserve PyPI package. \ No newline at end of file diff --git a/LICENSE b/LICENSE index 7a25446..0967b9f 100644 --- a/LICENSE +++ b/LICENSE @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 9b801f7..160820f 100644 --- a/README.md +++ b/README.md @@ -51,4 +51,4 @@ uvx fastapi-new ## License -This project is licensed under the terms of the MIT license. +This project is licensed under the terms of the MIT license. \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index a7aa580..fb5bff6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -113,9 +113,19 @@ ignore = [ # Preserve types, even if a file imports `from __future__ import annotations`. keep-runtime-typing = true +[tool.uv.workspace] +members = [ + "iya", +] + +[dependency-groups] +dev = [ + "coverage>=7.11.2", +] + [tool.pdm.build] source-includes = [ "tests/", "requirements*.txt", "scripts/", -] +] \ No newline at end of file diff --git a/requirements-tests.txt b/requirements-tests.txt deleted file mode 100644 index 62b5912..0000000 --- a/requirements-tests.txt +++ /dev/null @@ -1,7 +0,0 @@ --e . - -pytest >=8.3.5 -coverage[toml] >=7.6.1 -mypy ==1.18.2 -ruff ==0.14.4 -smokeshow >=0.5.0 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c5bd5e0 Binary files /dev/null and b/requirements.txt differ diff --git a/scripts/coverage.sh b/scripts/coverage.sh index e07b51e..3ef9098 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -5,4 +5,4 @@ set -x coverage combine coverage report -coverage html +coverage html \ No newline at end of file diff --git a/scripts/format.sh b/scripts/format.sh index 1a304f6..a25f3e2 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -2,4 +2,4 @@ set -x ruff check src tests scripts --fix -ruff format src tests scripts +ruff format src tests scripts \ No newline at end of file diff --git a/scripts/lint.sh b/scripts/lint.sh index 48a5628..64da0e6 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -5,4 +5,4 @@ set -x mypy src ruff check src tests scripts -ruff format src tests --check +ruff format src tests --check \ No newline at end of file diff --git a/scripts/test-cov-html.sh b/scripts/test-cov-html.sh index f87f906..3c77621 100755 --- a/scripts/test-cov-html.sh +++ b/scripts/test-cov-html.sh @@ -4,4 +4,4 @@ set -e set -x bash scripts/test.sh ${@} -bash scripts/coverage.sh +bash scripts/coverage.sh \ No newline at end of file diff --git a/scripts/test.sh b/scripts/test.sh index 109425d..f97e8c9 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -3,4 +3,4 @@ set -e set -x -coverage run -m pytest tests ${@} +coverage run -m pytest tests ${@} \ No newline at end of file diff --git a/src/fastapi_new/__init__.py b/src/fastapi_new/__init__.py index 3b93d0b..a0235ce 100644 --- a/src/fastapi_new/__init__.py +++ b/src/fastapi_new/__init__.py @@ -1 +1 @@ -__version__ = "0.0.2" +__version__ = "0.0.2" \ No newline at end of file diff --git a/src/fastapi_new/__main__.py b/src/fastapi_new/__main__.py index 4e28416..c49f9df 100644 --- a/src/fastapi_new/__main__.py +++ b/src/fastapi_new/__main__.py @@ -1,3 +1,3 @@ from .cli import main -main() +main() \ No newline at end of file diff --git a/src/fastapi_new/cli.py b/src/fastapi_new/cli.py index 4a8ad95..8ac7a8a 100644 --- a/src/fastapi_new/cli.py +++ b/src/fastapi_new/cli.py @@ -1,11 +1,8 @@ -import typer - from fastapi_new.new import new as new_command +from typer import Typer -app = typer.Typer(rich_markup_mode="rich") - +app = Typer(rich_markup_mode="rich") app.command()(new_command) - def main() -> None: - app() + app() \ No newline at end of file diff --git a/src/fastapi_new/constants/template.py b/src/fastapi_new/constants/template.py new file mode 100644 index 0000000..55d89c5 --- /dev/null +++ b/src/fastapi_new/constants/template.py @@ -0,0 +1,137 @@ +from textwrap import dedent + +TEMPLATE_DB_CONNECTION = dedent(""" +from dotenv import load_dotenv +from sqlalchemy import create_engine +from sqlalchemy.orm import DeclarativeBase, sessionmaker +import os + +# Load environment variables from .env file +load_dotenv() + +DATABASE_URL = os.getenv("DATABASE_URL") + +engine = create_engine(str(DATABASE_URL), echo=True) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +class Base(DeclarativeBase): + pass + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() +""").strip() + +TEMPLATE_ENV = """ +# Option 1: SQLite (Default - No setup required) +# DATABASE_URL=sqlite:///./{project_name}.db + +# Option 2: MySQL (Requires: uv add pymysql) +DATABASE_URL=mysql+pymysql://root:password@localhost:3306/{project_name} + +# Option 3: PostgreSQL (Requires: uv add psycopg2-binary) +# DATABASE_URL=postgresql://postgres:password@localhost:5432/{project_name} +""" + +TEMPLATE_MAIN = dedent(""" +from fastapi import FastAPI +# from fastapi.staticfiles import StaticFiles + +app = FastAPI() + +# Mount static files (if views folder exists) +# app.mount("/static", StaticFiles(directory="views/css"), name="static") + +@app.get("/") +def main(): + return {"message": "Welcome to your FastAPI project!"} +""").strip() + +TEMPLATE_HTML = dedent(""" + + + + + + FastAPI View + + + +

Hello from FastAPI Views! ๐Ÿš€

+ + + +""").strip() + +TEMPLATE_CSS = dedent(""" +body { + font-family: sans-serif; + background-color: #f0fdf4; /* Green-50 */ + color: #166534; /* Green-800 */ + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; +} +""").strip() + +TEMPLATE_JS = dedent(""" +console.log("FastAPI Views are active!"); +""").strip() + +TEMPLATE_GITIGNORE = dedent(""" +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Distribution / packaging +build/ +dist/ +*.egg-info/ +*.egg + +# Environment variables +.env + +# Testing / coverage +htmlcov/ +.coverage +.coverage.* +coverage/ +.pytest_cache/ +tox/ + +# Type checking +.mypy_cache/ + +# Virtual environments +.venv +venv/ + +# IDE +.idea/ + +# OS +.DS_Store +Thumbs.db +""").strip() + +TEMPLATE_RUFF = dedent(""" +# .ruff.toml - Standalone configuration +line-length = 88 +target-version = "py310" + +[lint] +select = ["E", "F", "I"] +ignore = [] +""").strip() + +TEMPLATE_TESTING = dedent(""" +def test_example(): + assert True +""").strip() \ No newline at end of file diff --git a/src/fastapi_new/core/__init__.py b/src/fastapi_new/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/fastapi_new/core/config.py b/src/fastapi_new/core/config.py new file mode 100644 index 0000000..c9303e8 --- /dev/null +++ b/src/fastapi_new/core/config.py @@ -0,0 +1,14 @@ +from dataclasses import dataclass +from pathlib import Path + + +@dataclass +class ProjectConfig: + name: str + path: Path + linter: str = "none" + orm: str = "none" + python: str | None = None + structure: str = "simple" + tests: bool = False + views: bool = False \ No newline at end of file diff --git a/src/fastapi_new/core/generator.py b/src/fastapi_new/core/generator.py new file mode 100644 index 0000000..a2a7d52 --- /dev/null +++ b/src/fastapi_new/core/generator.py @@ -0,0 +1,256 @@ +from fastapi_new.core.config import ProjectConfig +from fastapi_new.constants.template import * +from rich_toolkit import RichToolkit +from textwrap import dedent +import pathlib +import subprocess +import typer + + +def generate_readme(project_name: str) -> str: + return dedent(f""" + # {project_name} + + A project created with FastAPI CLI (Custom Wizard). + + ## ๐Ÿ“‚ Structure + + - **app/controllers**: Routes & Endpoints logic + - **app/models**: Database models (Pydantic/SQLModel) + - **app/schemas**: Data validation schemas + - **database**: Migrations & Seeders + - **tests**: Unit & Integration tests + + ## ๐Ÿš€ Quick Start + + 1. Activate environment: + - Windows: `.venv\\Scripts\\activate` + - Mac/Linux: `source .venv/bin/activate` + 2. Run server: + `uv run fastapi dev` + """).strip() + + +def exit_with_error(toolkit: RichToolkit, error_msg: str) -> None: + """ + Exit the program with an error message. + + Args: + toolkit (RichToolkit): The RichToolkit instance for printing messages. + error_msg (str): The error message to display. + + Returns: + None + """ + toolkit.print(f"[bold red]Error:[/bold red] {error_msg}", tag="error") + raise typer.Exit(code=1) + + +def validate_python_version(python: str | None) -> str | None: + """ + Validate the specified Python version. + + Args: + python (str|None): The Python version string. + + Returns: + str|None: An error message if the version is unsupported, otherwise None. + """ + if not python: + return None + try: + parts = python.split(".") + if len(parts) < 2: + return None + major, minor = int(parts[0]), int(parts[1]) + if major < 3 or (major == 3 and minor < 10): + return f"Python {python} is not supported. FastAPI requires Python 3.10+." + except (ValueError, IndexError): + pass + return None + + +def setup_environment(toolkit: RichToolkit, config: ProjectConfig) -> None: + """ + Set up the project environment using 'uv'. + + Args: + toolkit (RichToolkit): The RichToolkit instance for printing messages. + config (ProjectConfig): The project configuration. + + Returns: + None + """ + error = validate_python_version(config.python) + if error: + exit_with_error(toolkit, error) + + msg = "Setting up environment with uv" + if config.python: + msg += f" (Python {config.python})" + toolkit.print(msg, tag="env") + + init_cmd = ["uv", "init", "--bare", "--no-workspace"] + if not config.path.exists(): + config.path.mkdir(parents=True, exist_ok=True) + + if config.python: + init_cmd.extend(["--python", config.python]) + + try: + subprocess.run(init_cmd, check=True, capture_output=True, cwd=config.path) + except subprocess.CalledProcessError as e: + exit_with_error(toolkit, f"Failed to init uv: {e.stderr.decode() if e.stderr else str(e)}") + + +def install_dependencies(toolkit: RichToolkit, config: ProjectConfig) -> None: + """ + Install project dependencies and generate requirements.txt. + + Args: + toolkit (RichToolkit): The RichToolkit instance for printing messages. + config (ProjectConfig): The project configuration. + + Returns: + None + """ + toolkit.print("Installing dependencies & generating requirements.txt...", tag="deps") + try: + deps = ["fastapi[standard]", "python-dotenv"] + + if config.orm == "sqlmodel": + deps.append("sqlmodel") + elif config.orm == "sqlalchemy": + deps.append("sqlalchemy") + + if config.views: + deps.append("jinja2") + + if config.tests: + deps.append("pytest") + deps.append("httpx") + + if config.linter == "ruff": + deps.append("ruff") + elif config.linter == "classic": + deps.extend(["black", "isort", "flake8"]) + + subprocess.run(["uv", "add"] + deps, check=True, capture_output=True, cwd=config.path) + + with open(config.path / "requirements.txt", "w") as req_file: + subprocess.run( + ["uv", "export", "--format", "requirements-txt", "--no-hashes", "--no-header", "--no-annotate"], + stdout=req_file, + check=True, + cwd=config.path + ) + + except subprocess.CalledProcessError as e: + exit_with_error(toolkit, f"Failed to install deps: {e.stderr.decode() if e.stderr else str(e)}") + + +def create_file(path: pathlib.Path, content: str = "") -> None: + """ + Create a file with the given content. + + Args: + path (pathlib.Path): The file path to create. + content (str): The content to write to the file. + + Returns: + None + """ + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + + +def write_project_files(toolkit: RichToolkit, config: ProjectConfig) -> None: + """ + Write the project files based on the configuration. + Refactored for readability using helper functions. + + Args: + toolkit (RichToolkit): The RichToolkit instance for printing messages. + config (ProjectConfig): The project configuration. + + Returns: + None + """ + toolkit.print("Scaffolding project structure...", tag="template") + try: + _create_base_files(config) + _setup_git(config) + + if config.views: + _create_view_files(config) + + if config.orm != "none": + _configure_database(config) + + if config.structure == "advanced": + _setup_advanced_structure(config) + + hello_file = config.path / "hello.py" + if hello_file.exists(): + hello_file.unlink() + + except Exception as e: + exit_with_error(toolkit, f"Failed to write files: {str(e)}") + + +def _create_base_files(config: ProjectConfig) -> None: + """Create the fundamental files for the project.""" + create_file(config.path / "main.py", TEMPLATE_MAIN) + create_file(config.path / "README.md", generate_readme(config.name)) + + +def _setup_git(config: ProjectConfig) -> None: + """Create or update .gitignore.""" + gitignore_path = config.path / ".gitignore" + + if gitignore_path.exists(): + with open(gitignore_path, "a") as f: + f.write("\n" + TEMPLATE_GITIGNORE) + else: + create_file(gitignore_path, TEMPLATE_GITIGNORE) + + +def _create_view_files(config: ProjectConfig) -> None: + """Create HTML, CSS, and JS files.""" + base_view_path = config.path / "views" + create_file(base_view_path / "html" / "index.html", TEMPLATE_HTML) + create_file(base_view_path / "css" / "style.css", TEMPLATE_CSS) + create_file(base_view_path / "js" / "main.js", TEMPLATE_JS) + create_file(base_view_path / "assets" / ".gitkeep", "") + + +def _configure_database(config: ProjectConfig) -> None: + """Setup database connection, env file, and gitignore.""" + # Create database config + create_file(config.path / "config" / "database.py", TEMPLATE_DB_CONNECTION) + + # Create .env + env_content = TEMPLATE_ENV.format(project_name=config.name) + create_file(config.path / ".env", env_content.strip()) + + +def _setup_advanced_structure(config: ProjectConfig) -> None: + """Setup MVC folders, Migrations, Tests, and Linter config.""" + # 1. App Structure (MVC) + mvc_paths = ["app", "app/controllers", "app/models", "app/schemas"] + + for p in mvc_paths: + create_file(config.path / p / "__init__.py") + + # 2. Database Migrations + create_file(config.path / "database" / "migrations" / ".gitkeep") + create_file(config.path / "database" / "seeders" / ".gitkeep") + + # 3. Testing + if config.tests: + create_file(config.path / "tests" / "__init__.py") + create_file(config.path / "tests" / "test_main.py", TEMPLATE_TESTING) + + # 4. Linter + if config.linter == "ruff": + create_file(config.path / ".ruff.toml", TEMPLATE_RUFF) \ No newline at end of file diff --git a/src/fastapi_new/new.py b/src/fastapi_new/new.py index 9c255c9..673e83f 100644 --- a/src/fastapi_new/new.py +++ b/src/fastapi_new/new.py @@ -1,249 +1,35 @@ -import pathlib -import shutil -import subprocess -from dataclasses import dataclass +from .core.generator import * +from .ui.styles import get_rich_toolkit +from .ui.wizard import run_interactive_wizard from typing import Annotated - +import shutil import typer -from rich_toolkit import RichToolkit - -from .utils.cli import get_rich_toolkit - -TEMPLATE_CONTENT = """from fastapi import FastAPI -app = FastAPI() - -@app.get("/") -def main(): - return {"message": "Hello World"} -""" - - -@dataclass -class ProjectConfig: - name: str - path: pathlib.Path - python: str | None = None - - -def _generate_readme(project_name: str) -> str: - return f"""# {project_name} - -A project created with FastAPI CLI. - -## Quick Start - -### Start the development server: - -```bash -uv run fastapi dev -``` - -Visit http://localhost:8000 - -### Deploy to FastAPI Cloud: - -> Reader's note: These commands are not quite ready for prime time yet, but will be soon! Join the waiting list at https://fastapicloud.com! - -```bash -uv run fastapi login -uv run fastapi deploy -``` - -## Project Structure - -- `main.py` - Your FastAPI application -- `pyproject.toml` - Project dependencies - -## Learn More - -- [FastAPI Documentation](https://fastapi.tiangolo.com) -- [FastAPI Cloud](https://fastapicloud.com) -""" - - -def _exit_with_error(toolkit: RichToolkit, error_msg: str) -> None: - toolkit.print(f"[bold red]Error:[/bold red] {error_msg}", tag="error") - raise typer.Exit(code=1) - - -def _validate_python_version(python: str | None) -> str | None: - """ - Validate Python version is >= 3.10. - Returns error message if < 3.10, None otherwise. - Let uv handle malformed versions or versions it can't find. - """ - if not python: - return None - - try: - parts = python.split(".") - if len(parts) < 2: - return None # Let uv handle malformed version - major, minor = int(parts[0]), int(parts[1]) - - if major < 3 or (major == 3 and minor < 10): - return f"Python {python} is not supported. FastAPI requires Python 3.10 or higher." - except (ValueError, IndexError): - # Malformed version - let uv handle the error - pass - - return None - - -def _setup(toolkit: RichToolkit, config: ProjectConfig) -> None: - error = _validate_python_version(config.python) - if error: - _exit_with_error(toolkit, error) - - msg = "Setting up environment with uv" - - if config.python: - msg += f" (Python {config.python})" - - toolkit.print(msg, tag="env") - - # If config.name is provided, create in subdirectory; otherwise init in current dir - # uv will infer the project name from the directory name - if config.path == pathlib.Path.cwd(): - init_cmd = ["uv", "init", "--bare"] - else: - init_cmd = ["uv", "init", "--bare", config.name] - - if config.python: - init_cmd.extend(["--python", config.python]) - - try: - subprocess.run(init_cmd, check=True, capture_output=True) - except subprocess.CalledProcessError as e: - stderr = e.stderr.decode() if e.stderr else "No details available" - _exit_with_error(toolkit, f"Failed to initialize project with uv. {stderr}") - - -def _install_dependencies(toolkit: RichToolkit, config: ProjectConfig) -> None: - toolkit.print("Installing dependencies...", tag="deps") - - try: - subprocess.run( - ["uv", "add", "fastapi[standard]"], - check=True, - capture_output=True, - cwd=config.path, - ) - except subprocess.CalledProcessError as e: - stderr = e.stderr.decode() if e.stderr else "No details available" - _exit_with_error(toolkit, f"Failed to install dependencies. {stderr}") - - -def _write_template_files(toolkit: RichToolkit, config: ProjectConfig) -> None: - toolkit.print("Writing template files...", tag="template") - readme_content = _generate_readme(config.name) - - try: - (config.path / "main.py").write_text(TEMPLATE_CONTENT) - (config.path / "README.md").write_text(readme_content) - except Exception as e: - _exit_with_error(toolkit, f"Failed to write template files. {str(e)}") def new( - ctx: typer.Context, - project_name: Annotated[ - str | None, - typer.Argument( - help="The name of the new FastAPI project. If not provided, initializes in the current directory.", - ), - ] = None, - python: Annotated[ - str | None, - typer.Option( - "--python", - "-p", - help="Specify the Python version for the new project (e.g., 3.14). Must be 3.10 or higher.", - ), - ] = None, + project_name: Annotated[str | None, typer.Argument()] = None, + python: Annotated[str | None, typer.Option()] = None, ) -> None: - if project_name: - name = project_name - path = pathlib.Path.cwd() / project_name - else: - name = pathlib.Path.cwd().name - path = pathlib.Path.cwd() - - config = ProjectConfig( - name=name, - path=path, - python=python, - ) - with get_rich_toolkit() as toolkit: - toolkit.print_title("Creating a new project ๐Ÿš€", tag="FastAPI") - - toolkit.print_line() - - if not project_name: - toolkit.print( - f"[yellow]โš ๏ธ No project name provided. Initializing in current directory: {path}[/yellow]", - tag="warning", - ) - toolkit.print_line() - - # Check if project directory already exists (only for new subdirectory) - if project_name and config.path.exists(): - _exit_with_error(toolkit, f"Directory '{project_name}' already exists.") - if shutil.which("uv") is None: - _exit_with_error( - toolkit, - "uv is required to create new projects. Install it from https://docs.astral.sh/uv/getting-started/installation/", - ) + exit_with_error(toolkit, "uv is required. Please install uv first.") - _setup(toolkit, config) - - toolkit.print_line() + config = run_interactive_wizard(default_name=project_name) - _install_dependencies(toolkit, config) + if python: + config.python = python - toolkit.print_line() - - _write_template_files(toolkit, config) - - toolkit.print_line() - - # Print success message - if project_name: - toolkit.print( - f"[bold green]โœจ Success![/bold green] Created FastAPI project: [cyan]{project_name}[/cyan]", - tag="success", - ) - - toolkit.print_line() - - toolkit.print("[bold]Next steps:[/bold]") - toolkit.print(f" [dim]$[/dim] cd {project_name}") - toolkit.print(" [dim]$[/dim] uv run fastapi dev") - else: - toolkit.print( - "[bold green]โœจ Success![/bold green] Initialized FastAPI project in current directory", - tag="success", - ) - - toolkit.print_line() - - toolkit.print("[bold]Next steps:[/bold]") - toolkit.print(" [dim]$[/dim] uv run fastapi dev") - - toolkit.print_line() - - toolkit.print("Visit [blue]http://localhost:8000[/blue]") - - toolkit.print_line() + toolkit.print_title("Creating a new project ๐Ÿš€", tag="FastAPI") - toolkit.print("[bold]Deploy to FastAPI Cloud:[/bold]") - toolkit.print(" [dim]$[/dim] uv run fastapi login") - toolkit.print(" [dim]$[/dim] uv run fastapi deploy") + setup_environment(toolkit, config) + install_dependencies(toolkit, config) + write_project_files(toolkit, config) toolkit.print_line() - toolkit.print( - "[dim]๐Ÿ’ก Tip: Use 'uv run' to automatically use the project's environment[/dim]" + f"[bold green]โœจ Success![/bold green] Created project: [cyan]{config.name}[/cyan]", + tag="success", ) + + toolkit.print(f" [dim]$[/dim] cd {config.name}") + toolkit.print(" [dim]$[/dim] uv run fastapi dev") \ No newline at end of file diff --git a/src/fastapi_new/ui/__init__.py b/src/fastapi_new/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/fastapi_new/utils/cli.py b/src/fastapi_new/ui/styles.py similarity index 88% rename from src/fastapi_new/utils/cli.py rename to src/fastapi_new/ui/styles.py index b8737ab..fa26be4 100644 --- a/src/fastapi_new/utils/cli.py +++ b/src/fastapi_new/ui/styles.py @@ -1,9 +1,8 @@ -import logging - -from rich_toolkit import RichToolkit, RichToolkitTheme +from logging import getLogger from rich_toolkit.styles import MinimalStyle, TaggedStyle +from rich_toolkit import RichToolkit, RichToolkitTheme -logger = logging.getLogger(__name__) +logger = getLogger(__name__) class FastAPIStyle(TaggedStyle): @@ -28,4 +27,4 @@ def get_rich_toolkit(minimal: bool = False) -> RichToolkit: }, ) - return RichToolkit(theme=theme) + return RichToolkit(theme=theme) \ No newline at end of file diff --git a/src/fastapi_new/ui/wizard.py b/src/fastapi_new/ui/wizard.py new file mode 100644 index 0000000..6744ba9 --- /dev/null +++ b/src/fastapi_new/ui/wizard.py @@ -0,0 +1,66 @@ +from fastapi_new.core.config import ProjectConfig +from pathlib import Path +from rich.console import Console +from rich.prompt import Prompt, Confirm +from typer import Exit + + +def run_interactive_wizard(default_name: str | None = None) -> ProjectConfig: + console = Console() + + # Project Name + if default_name: + name = default_name + console.print(f"๐Ÿ“‚ Project Name: [bold green]{name}[/]") + else: + name = Prompt.ask("๐Ÿ“‚ What is your [bold green]Project Name[/]") + + path = Path.cwd() / name + + if path.exists(): + console.print( + f"[bold red]Error:[/bold red] Directory '{name}' already exists.") + raise Exit(code=1) + + # Architecture + console.print("\n[bold]๐Ÿ—๏ธ Architecture[/]") + console.print(" [bold cyan]1.[/] Simple [dim](Flat structure, microservices)[/]") + console.print(" [bold cyan]2.[/] Advanced [dim](MVC structure, scalable)[/]") + struct_choice = Prompt.ask("Choose style", choices=["1", "2"], show_choices=False, default="1") + structure = "advanced" if struct_choice == "2" else "simple" + + # Database + console.print("\n[bold]๐Ÿ’ฝ Database / ORM[/]") + console.print(" [bold cyan]1.[/] SQLModel [dim](Recommended for FastAPI)[/]") + console.print(" [bold cyan]2.[/] SQLAlchemy [dim](Classic & Robust)[/]") + console.print(" [bold cyan]3.[/] None [dim](No database)[/]") + orm_choice = Prompt.ask("Select ORM", choices=["1", "2", "3"], show_choices=False, default="1") + orm_map = {"1": "sqlmodel", "2": "sqlalchemy", "3": "none"} + orm = orm_map[orm_choice] + + # Linting + console.print("\n[bold]โœจ Linting & Formatting[/]") + console.print(" [bold cyan]1.[/] Ruff [dim](Recommended - Blazing fast)[/]") + console.print(" [bold cyan]2.[/] Classic [dim](Black + Isort + Flake8)[/]") + console.print(" [bold cyan]3.[/] None [dim](Skip linting)[/]") + lint_choice = Prompt.ask("Select Linter", choices=["1", "2", "3"], show_choices=False, default="1") + linter_map = {"1": "ruff", "2": "classic", "3": "none"} + linter = linter_map[lint_choice] + + # Views + console.print("\n[bold]๐ŸŽจ Frontend / Views[/]") + include_views = Confirm.ask("Add Views (HTML/CSS/JS)?", default=False) + + # Testing + console.print("\n[bold]๐Ÿงช Quality Assurance[/]") + include_tests = Confirm.ask("Add Testing (Pytest)?", default=True) + + return ProjectConfig( + linter=linter, + name=name, + orm=orm, + path=path, + structure=structure, + tests=include_tests, + views=include_views, + ) \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py index 764aa65..547f366 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +1 @@ -# Tests for fastapi-new +# Tests for fastapi-new \ No newline at end of file diff --git a/tests/test_cli.py b/tests/test_cli.py index 1b07cfe..b4e9c7d 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,11 +1,11 @@ import subprocess import sys - def test_script() -> None: result = subprocess.run( [sys.executable, "-m", "coverage", "run", "-m", "fastapi_new", "--help"], capture_output=True, encoding="utf-8", ) - assert "Usage" in result.stdout + + assert "Usage" in result.stdout \ No newline at end of file diff --git a/tests/test_main.py b/tests/test_main.py index 115e0e8..3572551 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,5 +1,5 @@ from fastapi_new import __version__ -def test_version_var_exists() -> None: - assert isinstance(__version__, str) +def test_version_variable_exists() -> None: + assert isinstance(__version__, str) \ No newline at end of file diff --git a/tests/test_new.py b/tests/test_new.py index 5103b70..2c4cfaa 100644 --- a/tests/test_new.py +++ b/tests/test_new.py @@ -1,170 +1,128 @@ -import shutil -import subprocess +from fastapi_new.cli import app from pathlib import Path -from typing import Any - -import pytest from typer.testing import CliRunner - -from fastapi_new.cli import app +from unittest.mock import MagicMock +import pytest +import shutil runner = CliRunner() @pytest.fixture -def temp_project_dir(tmp_path: Path, monkeypatch: Any) -> Path: +def temp_project_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: """Create a temporary directory and cd into it.""" monkeypatch.chdir(tmp_path) return tmp_path -def _assert_project_created(project_path: Path) -> None: +def _assert_common_files(project_path: Path) -> None: + """Check the basic files that must always be present.""" assert (project_path / "main.py").exists() assert (project_path / "README.md").exists() + assert (project_path / "requirements.txt").exists() + assert (project_path / ".gitignore").exists() assert (project_path / "pyproject.toml").exists() -def test_creates_project_successfully(temp_project_dir: Path) -> None: - result = runner.invoke(app, ["my_fastapi_project"]) - - assert result.exit_code == 0 - project_path = temp_project_dir / "my_fastapi_project" - _assert_project_created(project_path) - assert "Success!" in result.output - assert "my_fastapi_project" in result.output +def test_wizard_full_features(temp_project_dir: Path) -> None: + """ + Simulation test 'All-in-One': + Advanced + SQLModel + Ruff + Views + Tests. + Input Order: + 1. Name: 'full_app' + 2. Arch: '2' (Advanced) + 3. ORM: '1' (SQLModel) + 4. Lint: '1' (Ruff) + 5. View: 'y' + 6. Test: 'y' + """ + user_inputs = "full_app\n2\n1\n1\ny\ny\n" + result = runner.invoke(app, [], input=user_inputs) -def test_creates_project_with_python_version(temp_project_dir: Path) -> None: - # Test long form - result = runner.invoke(app, ["project_long", "--python", "3.12"]) assert result.exit_code == 0 - project_path = temp_project_dir / "project_long" - _assert_project_created(project_path) - assert "3.12" in (project_path / "pyproject.toml").read_text() + project_path = temp_project_dir / "full_app" - # Test short form - result = runner.invoke(app, ["project_short", "-p", "3.11"]) - assert result.exit_code == 0 - project_path = temp_project_dir / "project_short" - assert "3.11" in (project_path / "pyproject.toml").read_text() + _assert_common_files(project_path) + assert (project_path / "app" / "controllers").exists() + assert (project_path / "database" / "migrations").exists() -def test_validates_template_file_contents(temp_project_dir: Path) -> None: - result = runner.invoke(app, ["sample_project"]) - assert result.exit_code == 0 + assert (project_path / "config" / "database.py").exists() + assert (project_path / ".env").exists() - project_path = temp_project_dir / "sample_project" + gitignore_content = (project_path / ".gitignore").read_text() + assert ".env" in gitignore_content - main_py_content = (project_path / "main.py").read_text() - assert "from fastapi import FastAPI" in main_py_content - assert "app = FastAPI()" in main_py_content + assert (project_path / "views" / "html" / "index.html").exists() - # Check README.md - readme_content = (project_path / "README.md").read_text() - assert "# sample_project" in readme_content - assert "A project created with FastAPI" in readme_content + assert (project_path / ".ruff.toml").exists() - # Check pyproject.toml - pyproject_content = (project_path / "pyproject.toml").read_text() - assert 'name = "sample-project"' in pyproject_content - assert "fastapi[standard]" in pyproject_content +def test_wizard_minimal(temp_project_dir: Path) -> None: + """ + Simulation test 'Minimalist': + Simple + No DB + No Lint + No Views. -def test_initializes_in_current_directory(temp_project_dir: Path) -> None: - result = runner.invoke(app, []) + Input Order: + 1. Name: 'mini_app' + 2. Arch: '1' (Simple) + 3. ORM: '3' (None) + 4. Lint: '3' (None) + 5. View: 'n' + 6. Test: 'n' + """ + user_inputs = "mini_app\n1\n3\n3\nn\nn\n" + result = runner.invoke(app, [], input=user_inputs) assert result.exit_code == 0 - assert "No project name provided" in result.output - assert "Initializing in current directory" in result.output - _assert_project_created(temp_project_dir) - - -def test_rejects_existing_directory(temp_project_dir: Path) -> None: - existing_dir = temp_project_dir / "existing_project" - existing_dir.mkdir() - - result = runner.invoke(app, ["existing_project"]) - assert result.exit_code == 1 - assert "Directory 'existing_project' already exists." in result.output - - -def test_rejects_python_below_3_10(temp_project_dir: Path) -> None: - result = runner.invoke(app, ["test_project", "--python", "3.9"]) - assert result.exit_code == 1 - assert "Python 3.9 is not supported" in result.output - assert "FastAPI requires Python 3.10" in result.output + project_path = temp_project_dir / "mini_app" + _assert_common_files(project_path) -def test_passes_single_digit_python_version_to_uv(temp_project_dir: Path) -> None: - result = runner.invoke(app, ["test_project", "--python", "3"]) - assert result.exit_code == 0 - project_path = temp_project_dir / "test_project" - _assert_project_created(project_path) + assert not (project_path / "app").exists() + assert not (project_path / "views").exists() + assert not (project_path / "config").exists() + assert not (project_path / ".env").exists() + assert not (project_path / ".ruff.toml").exists() -def test_passes_malformed_python_version_to_uv(temp_project_dir: Path) -> None: - result = runner.invoke(app, ["test_project", "--python", "abc.def"]) - # uv will reject this, we just verify we don't crash during validation - assert result.exit_code == 1 +def test_args_mode_defaults(temp_project_dir: Path) -> None: + """ + Test old way: `fastapi-new projectname`. + Should automatically use defaults (Simple, No DB, No Lint, No Views). + """ + user_inputs = "\n\n\nn\ny\n" + result = runner.invoke(app, ["fast_project"], input=user_inputs) -def test_creates_project_without_python_flag(temp_project_dir: Path) -> None: - result = runner.invoke(app, ["test_project"]) assert result.exit_code == 0 - project_path = temp_project_dir / "test_project" - _assert_project_created(project_path) - - -def test_failed_to_initialize_with_uv(monkeypatch: Any) -> None: - def mock_run(*args: Any, **kwargs: Any) -> None: - # Let the first check for 'uv' succeed, but fail on 'uv init' - if args[0][0] == "uv" and args[0][1] == "init": - raise subprocess.CalledProcessError( - 1, args[0], stderr=b"uv init failed for some reason" - ) + project_path = temp_project_dir / "fast_project" + + _assert_common_files(project_path) + assert not (project_path / "app").exists() - monkeypatch.setattr(subprocess, "run", mock_run) - result = runner.invoke(app, ["failing_project"]) - assert result.exit_code == 1 - assert "Failed to initialize project with uv" in result.output - - -def test_failed_to_add_dependencies(temp_project_dir: Path, monkeypatch: Any) -> None: - def mock_run(*args: Any, **kwargs: Any) -> None: - # Let 'uv init' succeed, but fail on 'uv add' - if args[0][0] == "uv" and args[0][1] == "add": - raise subprocess.CalledProcessError( - 1, args[0], stderr=b"Failed to resolve dependencies" - ) +def test_rejects_existing_directory(temp_project_dir: Path) -> None: + """Ensure the wizard rejects if the folder already exists.""" + (temp_project_dir / "duplicate").mkdir() - monkeypatch.setattr(subprocess, "run", mock_run) + user_inputs = "duplicate\n" + result = runner.invoke(app, [], input=user_inputs) - result = runner.invoke(app, ["failing_deps"]) assert result.exit_code == 1 - assert "Failed to install dependencies" in result.output + assert "already exists" in result.output -def test_file_write_failure(temp_project_dir: Path, monkeypatch: Any) -> None: - original_write_text = Path.write_text +def test_uv_missing_handler(temp_project_dir: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Simulation if the user does not have 'uv' installed.""" + monkeypatch.setattr(shutil, "which", MagicMock(return_value=None)) - def mock_write_text(self: Path, *args: Any, **kwargs: Any) -> None: - # Fail when trying to write README.md (let main.py succeed first) - if self.name == "README.md": - raise PermissionError("Permission denied") - original_write_text(self, *args, **kwargs) + user_inputs = "no_uv_app\n" + result = runner.invoke(app, [], input=user_inputs) - monkeypatch.setattr(Path, "write_text", mock_write_text) - - result = runner.invoke(app, ["test_write_fail"]) assert result.exit_code == 1 - assert "Failed to write template files" in result.output - + assert "uv is required" in result.output -def test_uv_not_installed(temp_project_dir: Path, monkeypatch: Any) -> None: - monkeypatch.setattr(shutil, "which", lambda _: None) - - result = runner.invoke(app, ["test_uv_missing_project"]) - assert result.exit_code == 1 - assert "uv is required to create new projects" in result.output - assert "https://docs.astral.sh/uv/" in result.output + project_path = temp_project_dir / "no_uv_app" + assert not project_path.exists() \ No newline at end of file diff --git a/uv.lock b/uv.lock index a030c92..2a1a47e 100644 --- a/uv.lock +++ b/uv.lock @@ -129,19 +129,18 @@ toml = [ [[package]] name = "exceptiongroup" -version = "1.3.0" +version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] [[package]] name = "fastapi-new" -version = "0.0.1" source = { editable = "." } dependencies = [ { name = "rich" }, @@ -158,6 +157,11 @@ dev = [ { name = "ruff" }, ] +[package.dev-dependencies] +dev = [ + { name = "coverage" }, +] + [package.metadata] requires-dist = [ { name = "coverage", extras = ["toml"], marker = "extra == 'dev'", specifier = ">=7.0.0" }, @@ -171,6 +175,9 @@ requires-dist = [ ] provides-extras = ["dev"] +[package.metadata.requires-dev] +dev = [{ name = "coverage", specifier = ">=7.11.2" }] + [[package]] name = "iniconfig" version = "2.3.0" @@ -442,4 +449,4 @@ source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, -] +] \ No newline at end of file