create-flask-react 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. create_flask_react/__init__.py +0 -0
  2. create_flask_react/main.py +50 -0
  3. create_flask_react/template/.env.example +10 -0
  4. create_flask_react/template/.gitignore +23 -0
  5. create_flask_react/template/Makefile +22 -0
  6. create_flask_react/template/backend/.dockerignore +3 -0
  7. create_flask_react/template/backend/Dockerfile +6 -0
  8. create_flask_react/template/backend/app/__init__.py +23 -0
  9. create_flask_react/template/backend/app/__pycache__/__init__.cpython-312.pyc +0 -0
  10. create_flask_react/template/backend/app/__pycache__/config.cpython-312.pyc +0 -0
  11. create_flask_react/template/backend/app/__pycache__/extensions.cpython-312.pyc +0 -0
  12. create_flask_react/template/backend/app/config.py +29 -0
  13. create_flask_react/template/backend/app/extensions.py +19 -0
  14. create_flask_react/template/backend/app/models/__init__.py +3 -0
  15. create_flask_react/template/backend/app/models/__pycache__/__init__.cpython-312.pyc +0 -0
  16. create_flask_react/template/backend/app/models/__pycache__/user.cpython-312.pyc +0 -0
  17. create_flask_react/template/backend/app/models/user.py +18 -0
  18. create_flask_react/template/backend/app/routes/__init__.py +16 -0
  19. create_flask_react/template/backend/app/routes/__pycache__/__init__.cpython-312.pyc +0 -0
  20. create_flask_react/template/backend/app/routes/__pycache__/auth.cpython-312.pyc +0 -0
  21. create_flask_react/template/backend/app/routes/__pycache__/main.cpython-312.pyc +0 -0
  22. create_flask_react/template/backend/app/routes/auth.py +107 -0
  23. create_flask_react/template/backend/app/routes/main.py +19 -0
  24. create_flask_react/template/backend/app/static/.gitkeep +0 -0
  25. create_flask_react/template/backend/migrations/README +1 -0
  26. create_flask_react/template/backend/migrations/__pycache__/env.cpython-312.pyc +0 -0
  27. create_flask_react/template/backend/migrations/alembic.ini +40 -0
  28. create_flask_react/template/backend/migrations/env.py +69 -0
  29. create_flask_react/template/backend/migrations/script.py.mako +24 -0
  30. create_flask_react/template/backend/migrations/versions/001_create_user_table.py +32 -0
  31. create_flask_react/template/backend/migrations/versions/__pycache__/001_create_user_table.cpython-312.pyc +0 -0
  32. create_flask_react/template/backend/pyproject.toml +13 -0
  33. create_flask_react/template/backend/tests/.gitkeep +0 -0
  34. create_flask_react/template/backend/uv.lock +561 -0
  35. create_flask_react/template/docker-compose.yml +45 -0
  36. create_flask_react/template/frontend/Dockerfile +5 -0
  37. create_flask_react/template/frontend/components.json +16 -0
  38. create_flask_react/template/frontend/index.html +12 -0
  39. create_flask_react/template/frontend/package.json +33 -0
  40. create_flask_react/template/frontend/postcss.config.js +6 -0
  41. create_flask_react/template/frontend/src/App.tsx +23 -0
  42. create_flask_react/template/frontend/src/api/client.ts +43 -0
  43. create_flask_react/template/frontend/src/components/ProtectedRoute.tsx +12 -0
  44. create_flask_react/template/frontend/src/components/ui/alert.tsx +58 -0
  45. create_flask_react/template/frontend/src/components/ui/button.tsx +55 -0
  46. create_flask_react/template/frontend/src/components/ui/card.tsx +85 -0
  47. create_flask_react/template/frontend/src/components/ui/input.tsx +24 -0
  48. create_flask_react/template/frontend/src/components/ui/label.tsx +23 -0
  49. create_flask_react/template/frontend/src/context/AuthContext.tsx +64 -0
  50. create_flask_react/template/frontend/src/index.css +37 -0
  51. create_flask_react/template/frontend/src/lib/utils.ts +6 -0
  52. create_flask_react/template/frontend/src/main.tsx +16 -0
  53. create_flask_react/template/frontend/src/pages/Dashboard.tsx +37 -0
  54. create_flask_react/template/frontend/src/pages/Login.tsx +88 -0
  55. create_flask_react/template/frontend/src/pages/Register.tsx +88 -0
  56. create_flask_react/template/frontend/src/vite-env.d.ts +1 -0
  57. create_flask_react/template/frontend/tailwind.config.ts +60 -0
  58. create_flask_react/template/frontend/tsconfig.json +25 -0
  59. create_flask_react/template/frontend/tsconfig.node.json +10 -0
  60. create_flask_react/template/frontend/vite.config.ts +24 -0
  61. create_flask_react-0.1.0.dist-info/METADATA +84 -0
  62. create_flask_react-0.1.0.dist-info/RECORD +66 -0
  63. create_flask_react-0.1.0.dist-info/WHEEL +5 -0
  64. create_flask_react-0.1.0.dist-info/entry_points.txt +2 -0
  65. create_flask_react-0.1.0.dist-info/licenses/LICENSE +21 -0
  66. create_flask_react-0.1.0.dist-info/top_level.txt +1 -0
File without changes
@@ -0,0 +1,50 @@
1
+ """CLI entry point for create-flask-react."""
2
+
3
+ import argparse
4
+ import shutil
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ TEMPLATE_DIR = Path(__file__).parent / "template"
9
+ IGNORE = shutil.ignore_patterns(
10
+ ".venv", "node_modules", "__pycache__", "*.pyc", ".env",
11
+ )
12
+
13
+
14
+ def main():
15
+ parser = argparse.ArgumentParser(
16
+ description="Create a new Flask + React project"
17
+ )
18
+ parser.add_argument(
19
+ "project_name",
20
+ help="Name of the project directory to create (use '.' for current directory)",
21
+ )
22
+ args = parser.parse_args()
23
+
24
+ if args.project_name == ".":
25
+ dest = Path.cwd()
26
+ else:
27
+ dest = Path.cwd() / args.project_name
28
+
29
+ if dest.exists() and args.project_name != ".":
30
+ if any(dest.iterdir()):
31
+ print(f"Error: {dest} already exists and is not empty.", file=sys.stderr)
32
+ sys.exit(1)
33
+
34
+ dest.mkdir(parents=True, exist_ok=True)
35
+
36
+ shutil.copytree(TEMPLATE_DIR, dest, dirs_exist_ok=True, ignore=IGNORE)
37
+
38
+ # Create .env from .env.example
39
+ env_example = dest / ".env.example"
40
+ env_file = dest / ".env"
41
+ if env_example.exists() and not env_file.exists():
42
+ shutil.copy2(env_example, env_file)
43
+
44
+ print(f"Created Flask + React project in {dest.resolve()}")
45
+ print()
46
+ print("Next steps:")
47
+ print(f" cd {args.project_name}")
48
+ print(" make dev")
49
+ print()
50
+ print("Then open http://localhost:5173 in your browser.")
@@ -0,0 +1,10 @@
1
+ # Database
2
+ POSTGRES_USER=app
3
+ POSTGRES_PASSWORD=changeme
4
+ POSTGRES_DB=app
5
+ DATABASE_URL=postgresql://app:changeme@db:5432/app
6
+
7
+ # Flask
8
+ SECRET_KEY=change-this-to-a-random-secret
9
+ FLASK_ENV=development
10
+ FLASK_DEBUG=1
@@ -0,0 +1,23 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .venv/
6
+
7
+ # Node
8
+ node_modules/
9
+ dist/
10
+
11
+ # Environment
12
+ .env
13
+
14
+ # IDE
15
+ .vscode/
16
+ .idea/
17
+
18
+ # OS
19
+ .DS_Store
20
+ Thumbs.db
21
+
22
+ # Docker
23
+ *.log
@@ -0,0 +1,22 @@
1
+ .PHONY: dev stop logs db-migrate db-upgrade shell clean
2
+
3
+ dev:
4
+ docker compose up --build
5
+
6
+ stop:
7
+ docker compose down
8
+
9
+ logs:
10
+ docker compose logs -f
11
+
12
+ db-migrate:
13
+ docker compose exec backend uv run flask db migrate
14
+
15
+ db-upgrade:
16
+ docker compose exec backend uv run flask db upgrade
17
+
18
+ shell:
19
+ docker compose exec backend uv run flask shell
20
+
21
+ clean:
22
+ docker compose down -v
@@ -0,0 +1,3 @@
1
+ .venv
2
+ __pycache__
3
+ *.pyc
@@ -0,0 +1,6 @@
1
+ FROM python:3.12-slim
2
+ WORKDIR /app
3
+ COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
4
+ COPY pyproject.toml uv.lock* ./
5
+ RUN uv sync
6
+ COPY . .
@@ -0,0 +1,23 @@
1
+ import os
2
+ from flask import Flask
3
+ from .config import config
4
+ from .extensions import db, migrate, login_manager
5
+
6
+
7
+ def create_app():
8
+ app = Flask(__name__, static_folder="static", static_url_path="/")
9
+
10
+ env = os.getenv("FLASK_ENV", "development")
11
+ app.config.from_object(config[env])
12
+
13
+ db.init_app(app)
14
+ migrate.init_app(app, db)
15
+ login_manager.init_app(app)
16
+
17
+ from .routes import api_bp
18
+ from .routes.main import main_bp
19
+
20
+ app.register_blueprint(api_bp)
21
+ app.register_blueprint(main_bp)
22
+
23
+ return app
@@ -0,0 +1,29 @@
1
+ import os
2
+
3
+
4
+ class BaseConfig:
5
+ SECRET_KEY = os.getenv("SECRET_KEY", "change-this-to-a-random-secret")
6
+ SQLALCHEMY_DATABASE_URI = os.getenv("DATABASE_URL")
7
+ SQLALCHEMY_TRACK_MODIFICATIONS = False
8
+ SESSION_COOKIE_SAMESITE = "Lax"
9
+ SESSION_COOKIE_HTTPONLY = True
10
+
11
+
12
+ class DevelopmentConfig(BaseConfig):
13
+ DEBUG = True
14
+
15
+
16
+ class ProductionConfig(BaseConfig):
17
+ DEBUG = False
18
+
19
+
20
+ class TestConfig(BaseConfig):
21
+ TESTING = True
22
+ SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:"
23
+
24
+
25
+ config = {
26
+ "development": DevelopmentConfig,
27
+ "production": ProductionConfig,
28
+ "testing": TestConfig,
29
+ }
@@ -0,0 +1,19 @@
1
+ from flask import jsonify
2
+ from flask_login import LoginManager
3
+ from flask_migrate import Migrate
4
+ from flask_sqlalchemy import SQLAlchemy
5
+
6
+ db = SQLAlchemy()
7
+ migrate = Migrate()
8
+ login_manager = LoginManager()
9
+
10
+
11
+ @login_manager.user_loader
12
+ def load_user(user_id):
13
+ from .models.user import User
14
+ return db.session.get(User, int(user_id))
15
+
16
+
17
+ @login_manager.unauthorized_handler
18
+ def unauthorized():
19
+ return jsonify({"error": "Authentication required"}), 401
@@ -0,0 +1,3 @@
1
+ from .user import User
2
+
3
+ __all__ = ["User"]
@@ -0,0 +1,18 @@
1
+ from datetime import datetime, timezone
2
+ from flask_login import UserMixin
3
+ from werkzeug.security import generate_password_hash, check_password_hash
4
+ from ..extensions import db
5
+
6
+
7
+ class User(UserMixin, db.Model):
8
+ id = db.Column(db.Integer, primary_key=True)
9
+ email = db.Column(db.String(255), unique=True, nullable=False, index=True)
10
+ password_hash = db.Column(db.String(256), nullable=False)
11
+ is_active = db.Column(db.Boolean, default=True)
12
+ created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
13
+
14
+ def set_password(self, password):
15
+ self.password_hash = generate_password_hash(password)
16
+
17
+ def check_password(self, password):
18
+ return check_password_hash(self.password_hash, password)
@@ -0,0 +1,16 @@
1
+ from flask import Blueprint
2
+ from flask_restx import Api
3
+
4
+ api_bp = Blueprint("api", __name__, url_prefix="/api")
5
+
6
+ api = Api(
7
+ api_bp,
8
+ version="1.0",
9
+ title="API",
10
+ description="Flask + React Starter API",
11
+ doc="/docs",
12
+ )
13
+
14
+ from .auth import ns as auth_ns # noqa: E402
15
+
16
+ api.add_namespace(auth_ns)
@@ -0,0 +1,107 @@
1
+ from flask_login import current_user, login_required, login_user, logout_user
2
+ from flask_restx import Namespace, Resource, fields
3
+ from ..extensions import db
4
+ from ..models.user import User
5
+
6
+ ns = Namespace("auth", description="Authentication")
7
+
8
+ credentials_model = ns.model("Credentials", {
9
+ "email": fields.String(required=True, description="User email"),
10
+ "password": fields.String(required=True, description="User password"),
11
+ })
12
+
13
+ user_model = ns.model("User", {
14
+ "id": fields.Integer(description="User ID"),
15
+ "email": fields.String(description="User email"),
16
+ "created_at": fields.DateTime(description="Account creation date"),
17
+ })
18
+
19
+ error_model = ns.model("Error", {
20
+ "error": fields.String(description="Error message"),
21
+ })
22
+
23
+ message_model = ns.model("Message", {
24
+ "message": fields.String(description="Status message"),
25
+ })
26
+
27
+
28
+ @ns.route("/register")
29
+ class Register(Resource):
30
+ @ns.expect(credentials_model)
31
+ @ns.response(201, "User created", user_model)
32
+ @ns.response(400, "Validation error", error_model)
33
+ @ns.response(409, "Email already registered", error_model)
34
+ def post(self):
35
+ """Register a new user."""
36
+ data = ns.payload
37
+ email = data.get("email")
38
+ password = data.get("password")
39
+
40
+ if not email or not password:
41
+ return {"error": "Email and password are required"}, 400
42
+
43
+ if User.query.filter_by(email=email).first():
44
+ return {"error": "Email already registered"}, 409
45
+
46
+ user = User(email=email)
47
+ user.set_password(password)
48
+ db.session.add(user)
49
+ db.session.commit()
50
+
51
+ login_user(user)
52
+
53
+ return {
54
+ "id": user.id,
55
+ "email": user.email,
56
+ "created_at": user.created_at.isoformat(),
57
+ }, 201
58
+
59
+
60
+ @ns.route("/login")
61
+ class Login(Resource):
62
+ @ns.expect(credentials_model)
63
+ @ns.response(200, "Login successful", user_model)
64
+ @ns.response(401, "Invalid credentials", error_model)
65
+ def post(self):
66
+ """Log in with email and password."""
67
+ data = ns.payload
68
+ email = data.get("email")
69
+ password = data.get("password")
70
+
71
+ user = User.query.filter_by(email=email).first()
72
+
73
+ if user is None or not user.check_password(password):
74
+ return {"error": "Invalid email or password"}, 401
75
+
76
+ login_user(user)
77
+
78
+ return {
79
+ "id": user.id,
80
+ "email": user.email,
81
+ "created_at": user.created_at.isoformat(),
82
+ }, 200
83
+
84
+
85
+ @ns.route("/logout")
86
+ class Logout(Resource):
87
+ @login_required
88
+ @ns.response(200, "Logged out", message_model)
89
+ @ns.response(401, "Not authenticated", error_model)
90
+ def post(self):
91
+ """Log out the current user."""
92
+ logout_user()
93
+ return {"message": "Logged out"}, 200
94
+
95
+
96
+ @ns.route("/me")
97
+ class Me(Resource):
98
+ @login_required
99
+ @ns.response(200, "Current user", user_model)
100
+ @ns.response(401, "Not authenticated", error_model)
101
+ def get(self):
102
+ """Get the current authenticated user."""
103
+ return {
104
+ "id": current_user.id,
105
+ "email": current_user.email,
106
+ "created_at": current_user.created_at.isoformat(),
107
+ }, 200
@@ -0,0 +1,19 @@
1
+ import os
2
+ from flask import Blueprint, send_from_directory, current_app
3
+
4
+ main_bp = Blueprint("main", __name__)
5
+
6
+
7
+ @main_bp.route("/", defaults={"path": ""})
8
+ @main_bp.route("/<path:path>")
9
+ def serve(path):
10
+ static_folder = os.path.join(current_app.root_path, "static")
11
+
12
+ if path and os.path.exists(os.path.join(static_folder, path)):
13
+ return send_from_directory(static_folder, path)
14
+
15
+ index_path = os.path.join(static_folder, "index.html")
16
+ if os.path.exists(index_path):
17
+ return send_from_directory(static_folder, "index.html")
18
+
19
+ return "Frontend not built. Run the Vite dev server or build the frontend.", 404
@@ -0,0 +1 @@
1
+ Single-database configuration for Flask.
@@ -0,0 +1,40 @@
1
+ [alembic]
2
+
3
+ [loggers]
4
+ keys = root,sqlalchemy,alembic,flask_migrate
5
+
6
+ [handlers]
7
+ keys = console
8
+
9
+ [formatters]
10
+ keys = generic
11
+
12
+ [logger_root]
13
+ level = WARN
14
+ handlers = console
15
+ qualname =
16
+
17
+ [logger_sqlalchemy]
18
+ level = WARN
19
+ handlers =
20
+ qualname = sqlalchemy.engine
21
+
22
+ [logger_alembic]
23
+ level = INFO
24
+ handlers =
25
+ qualname = alembic
26
+
27
+ [logger_flask_migrate]
28
+ level = INFO
29
+ handlers =
30
+ qualname = flask_migrate
31
+
32
+ [handler_console]
33
+ class = StreamHandler
34
+ args = (sys.stderr,)
35
+ level = NOTSET
36
+ formatter = generic
37
+
38
+ [formatter_generic]
39
+ format = %(levelname)-5.5s [%(name)s] %(message)s
40
+ datefmt = %H:%M:%S
@@ -0,0 +1,69 @@
1
+ import logging
2
+ from logging.config import fileConfig
3
+
4
+ from flask import current_app
5
+ from alembic import context
6
+
7
+ config = context.config
8
+ fileConfig(config.config_file_name)
9
+ logger = logging.getLogger("alembic.env")
10
+
11
+
12
+ def get_engine():
13
+ try:
14
+ return current_app.extensions["migrate"].db.get_engine()
15
+ except (TypeError, AttributeError):
16
+ return current_app.extensions["migrate"].db.engine
17
+
18
+
19
+ def get_engine_url():
20
+ try:
21
+ return (
22
+ get_engine()
23
+ .url.render_as_string(hide_password=False)
24
+ .replace("%", "%%")
25
+ )
26
+ except AttributeError:
27
+ return str(get_engine().url).replace("%", "%%")
28
+
29
+
30
+ config.set_main_option("sqlalchemy.url", get_engine_url())
31
+ target_db = current_app.extensions["migrate"].db
32
+
33
+
34
+ def get_metadata():
35
+ if hasattr(target_db, "metadatas"):
36
+ return target_db.metadatas[None]
37
+ return target_db.metadata
38
+
39
+
40
+ def run_migrations_offline():
41
+ url = config.get_main_option("sqlalchemy.url")
42
+ context.configure(url=url, target_metadata=get_metadata(), literal_binds=True)
43
+ with context.begin_transaction():
44
+ context.run_migrations()
45
+
46
+
47
+ def run_migrations_online():
48
+ def process_revision_directives(context, revision, directives):
49
+ if getattr(config.cmd_opts, "autogenerate", False):
50
+ script = directives[0]
51
+ if script.upgrade_ops.is_empty():
52
+ directives[:] = []
53
+ logger.info("No changes in schema detected.")
54
+
55
+ connectable = get_engine()
56
+ with connectable.connect() as connection:
57
+ context.configure(
58
+ connection=connection,
59
+ target_metadata=get_metadata(),
60
+ process_revision_directives=process_revision_directives,
61
+ )
62
+ with context.begin_transaction():
63
+ context.run_migrations()
64
+
65
+
66
+ if context.is_offline_mode():
67
+ run_migrations_offline()
68
+ else:
69
+ run_migrations_online()
@@ -0,0 +1,24 @@
1
+ """${message}
2
+
3
+ Revision ID: ${up_revision}
4
+ Revises: ${down_revision | comma,n}
5
+ Create Date: ${create_date}
6
+
7
+ """
8
+ from alembic import op
9
+ import sqlalchemy as sa
10
+ ${imports if imports else ""}
11
+
12
+ # revision identifiers, used by Alembic.
13
+ revision = ${repr(up_revision)}
14
+ down_revision = ${repr(down_revision)}
15
+ branch_labels = ${repr(branch_labels)}
16
+ depends_on = ${repr(depends_on)}
17
+
18
+
19
+ def upgrade():
20
+ ${upgrades if upgrades else "pass"}
21
+
22
+
23
+ def downgrade():
24
+ ${downgrades if downgrades else "pass"}
@@ -0,0 +1,32 @@
1
+ """create user table
2
+
3
+ Revision ID: 001
4
+ Revises:
5
+ Create Date: 2024-01-01 00:00:00.000000
6
+
7
+ """
8
+ from alembic import op
9
+ import sqlalchemy as sa
10
+
11
+ revision = "001"
12
+ down_revision = None
13
+ branch_labels = None
14
+ depends_on = None
15
+
16
+
17
+ def upgrade():
18
+ op.create_table(
19
+ "user",
20
+ sa.Column("id", sa.Integer(), nullable=False),
21
+ sa.Column("email", sa.String(length=255), nullable=False),
22
+ sa.Column("password_hash", sa.String(length=256), nullable=False),
23
+ sa.Column("is_active", sa.Boolean(), nullable=True),
24
+ sa.Column("created_at", sa.DateTime(), nullable=True),
25
+ sa.PrimaryKeyConstraint("id"),
26
+ )
27
+ op.create_index(op.f("ix_user_email"), "user", ["email"], unique=True)
28
+
29
+
30
+ def downgrade():
31
+ op.drop_index(op.f("ix_user_email"), table_name="user")
32
+ op.drop_table("user")
@@ -0,0 +1,13 @@
1
+ [project]
2
+ name = "backend"
3
+ version = "0.1.0"
4
+ requires-python = ">=3.12"
5
+ dependencies = [
6
+ "flask>=3.0",
7
+ "flask-login>=0.6",
8
+ "flask-restx>=1.3",
9
+ "flask-sqlalchemy>=3.1",
10
+ "flask-migrate>=4.0",
11
+ "psycopg2-binary>=2.9",
12
+ "werkzeug>=3.0",
13
+ ]
File without changes