@fluxyr/quill-stack-flask 0.1.0

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.
@@ -0,0 +1,4 @@
1
+ import { StackAdapter } from "@fluxyr/quill-core";
2
+ export declare const flaskStack: StackAdapter;
3
+ export default flaskStack;
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAUA,OAAO,EAEL,YAAY,EAMb,MAAM,oBAAoB,CAAC;AAO5B,eAAO,MAAM,UAAU,EAAE,YAexB,CAAC;AAIF,eAAe,UAAU,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.flaskStack = void 0;
37
+ /**
38
+ * @fluxyr/quill-stack-flask — the reference stack: Python/Flask/SQLAlchemy, flavored
39
+ * after fluxyr (Base-62 short IDs, BaseModel, account-scoped blueprints).
40
+ *
41
+ * Importing this module registers the "flask" stack with @fluxyr/quill-core. Core
42
+ * conventions are shared (owned by @fluxyr/quill-core); this stack contributes only the
43
+ * framework binding, the scaffold templates, and verify().
44
+ */
45
+ const fs = __importStar(require("fs"));
46
+ const path = __importStar(require("path"));
47
+ const quill_core_1 = require("@fluxyr/quill-core");
48
+ const verify_1 = require("./verify");
49
+ const PKG_ROOT = path.resolve(__dirname, "..");
50
+ const BINDING = path.join(PKG_ROOT, "prompts", "binding.md");
51
+ const TEMPLATES = path.join(PKG_ROOT, "templates");
52
+ exports.flaskStack = {
53
+ name: "flask",
54
+ scaffoldTemplates() {
55
+ return (0, quill_core_1.readTemplatesDir)(TEMPLATES);
56
+ },
57
+ buildSystemPrompt(ctx) {
58
+ const binding = fs.existsSync(BINDING) ? fs.readFileSync(BINDING, "utf8") : "";
59
+ return (0, quill_core_1.composeStackSystemPrompt)(ctx.corePrompts, binding);
60
+ },
61
+ verify(generatedDir) {
62
+ return (0, verify_1.verifyFlask)(generatedDir);
63
+ },
64
+ };
65
+ (0, quill_core_1.registerStack)(exports.flaskStack);
66
+ exports.default = exports.flaskStack;
67
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;;;;;;;GAOG;AACH,uCAAyB;AACzB,2CAA6B;AAC7B,mDAQ4B;AAC5B,qCAAuC;AAEvC,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;AAC/C,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,SAAS,EAAE,YAAY,CAAC,CAAC;AAC7D,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;AAEtC,QAAA,UAAU,GAAiB;IACtC,IAAI,EAAE,OAAO;IAEb,iBAAiB;QACf,OAAO,IAAA,6BAAgB,EAAC,SAAS,CAAC,CAAC;IACrC,CAAC;IAED,iBAAiB,CAAC,GAAiB;QACjC,MAAM,OAAO,GAAG,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/E,OAAO,IAAA,qCAAwB,EAAC,GAAG,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IAC5D,CAAC;IAED,MAAM,CAAC,YAAoB;QACzB,OAAO,IAAA,oBAAW,EAAC,YAAY,CAAC,CAAC;IACnC,CAAC;CACF,CAAC;AAEF,IAAA,0BAAa,EAAC,kBAAU,CAAC,CAAC;AAE1B,kBAAe,kBAAU,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { VerifyResult } from "@fluxyr/quill-core";
2
+ export declare function verifyFlask(generatedDir: string): Promise<VerifyResult>;
3
+ //# sourceMappingURL=verify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AA6ClD,wBAAsB,WAAW,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAiE7E"}
package/dist/verify.js ADDED
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.verifyFlask = verifyFlask;
37
+ /**
38
+ * Verification for the Flask stack — feeds the repair loop.
39
+ *
40
+ * Stages, in order:
41
+ * 1. Syntax check (compileall) — always runs; hard gate.
42
+ * 2. Import / app-factory smoke — runs if Flask is importable.
43
+ * 3. pytest — runs if Flask + pytest are importable.
44
+ *
45
+ * When the app's dependencies are not installed, stages 2-3 are skipped (a soft
46
+ * pass) rather than reported as failures, so the repair loop never fights a
47
+ * missing-deps condition. Install deps (see requirements.txt) for full checks.
48
+ */
49
+ const child_process_1 = require("child_process");
50
+ const fs = __importStar(require("fs"));
51
+ const path = __importStar(require("path"));
52
+ function run(cmd, args, cwd) {
53
+ const r = (0, child_process_1.spawnSync)(cmd, args, {
54
+ cwd,
55
+ encoding: "utf8",
56
+ maxBuffer: 32 * 1024 * 1024,
57
+ env: process.env,
58
+ });
59
+ const out = `${r.stdout ?? ""}${r.stderr ?? ""}`.trim();
60
+ return { code: r.error ? 127 : r.status ?? 1, out };
61
+ }
62
+ /** Candidate interpreters, most specific first. */
63
+ function interpreters(generatedDir) {
64
+ const c = [];
65
+ if (process.env.QUILL_PY)
66
+ c.push(process.env.QUILL_PY);
67
+ const venv = path.join(generatedDir, ".venv", "bin", "python");
68
+ if (fs.existsSync(venv))
69
+ c.push(venv);
70
+ c.push("python3", "python");
71
+ return c;
72
+ }
73
+ /** Find an interpreter that can `import flask`, or null. */
74
+ function pyWithFlask(generatedDir) {
75
+ for (const py of interpreters(generatedDir)) {
76
+ const r = run(py, ["-c", "import flask"], generatedDir);
77
+ if (r.code === 0)
78
+ return py;
79
+ }
80
+ return null;
81
+ }
82
+ function anyPython(generatedDir) {
83
+ for (const py of interpreters(generatedDir)) {
84
+ const r = run(py, ["--version"], generatedDir);
85
+ if (r.code === 0)
86
+ return py;
87
+ }
88
+ return null;
89
+ }
90
+ async function verifyFlask(generatedDir) {
91
+ const basePy = anyPython(generatedDir);
92
+ if (!basePy) {
93
+ return {
94
+ ok: false,
95
+ summary: "no python interpreter found",
96
+ feedback: "No python3 interpreter is available to verify the app.",
97
+ };
98
+ }
99
+ // Stage 1 — syntax.
100
+ const targets = ["app", "tests", "wsgi.py", "config.py"].filter((t) => fs.existsSync(path.join(generatedDir, t)));
101
+ const compile = run(basePy, ["-m", "compileall", "-q", ...targets], generatedDir);
102
+ if (compile.code !== 0) {
103
+ return {
104
+ ok: false,
105
+ summary: "syntax error",
106
+ feedback: `Syntax check failed (compileall):\n${compile.out}`,
107
+ };
108
+ }
109
+ // Stages 2-3 require Flask.
110
+ const py = pyWithFlask(generatedDir);
111
+ if (!py) {
112
+ return {
113
+ ok: true,
114
+ summary: "syntax ok; deps not installed (import/tests skipped)",
115
+ feedback: "",
116
+ };
117
+ }
118
+ // Stage 2 — import / app factory smoke.
119
+ const smoke = run(py, ["-c", "import app; a = app.create_app(); print('factory ok')"], generatedDir);
120
+ if (smoke.code !== 0) {
121
+ return {
122
+ ok: false,
123
+ summary: "import/app-factory error",
124
+ feedback: `The app failed to import / build via create_app():\n${smoke.out}`,
125
+ };
126
+ }
127
+ // Stage 3 — pytest.
128
+ const hasTests = fs.existsSync(path.join(generatedDir, "tests")) &&
129
+ fs
130
+ .readdirSync(path.join(generatedDir, "tests"))
131
+ .some((f) => f.startsWith("test_") && f.endsWith(".py"));
132
+ if (!hasTests) {
133
+ return { ok: true, summary: "factory ok; no tests found", feedback: "" };
134
+ }
135
+ const pytest = run(py, ["-m", "pytest", "-q"], generatedDir);
136
+ if (pytest.code !== 0) {
137
+ return {
138
+ ok: false,
139
+ summary: "pytest failing",
140
+ feedback: `pytest failed:\n${pytest.out}`,
141
+ };
142
+ }
143
+ return { ok: true, summary: "syntax + factory + pytest pass", feedback: "" };
144
+ }
145
+ //# sourceMappingURL=verify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.js","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@fluxyr/quill-stack-flask",
3
+ "version": "0.1.0",
4
+ "description": "Quill reference stack: Python/Flask/SQLAlchemy (fluxyr-flavored).",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist",
9
+ "prompts",
10
+ "templates"
11
+ ],
12
+ "dependencies": {
13
+ "@fluxyr/quill-core": "0.1.0"
14
+ },
15
+ "license": "MIT",
16
+ "author": "Fluxyr",
17
+ "homepage": "https://github.com/fluxyr-app/quill#readme",
18
+ "bugs": {
19
+ "url": "https://github.com/fluxyr-app/quill/issues"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/fluxyr-app/quill.git",
24
+ "directory": "packages/stack-flask"
25
+ },
26
+ "keywords": [
27
+ "quill",
28
+ "code-generation",
29
+ "llm",
30
+ "micro-prompts",
31
+ "openrouter",
32
+ "scaffolding"
33
+ ],
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "engines": {
38
+ "node": ">=20"
39
+ }
40
+ }
@@ -0,0 +1,69 @@
1
+ # Framework binding — Python / Flask / SQLAlchemy
2
+
3
+ This app targets **Python + Flask + SQLAlchemy**. Realize the language-agnostic
4
+ conventions above in this framework as described here. This binding is owned by the
5
+ stack; the conventions are the durable, framework-independent spec.
6
+
7
+ ## Identifiers (short-ID scheme)
8
+
9
+ Implement the short-ID behavior in these files (none are shipped — create them):
10
+
11
+ - `app/utils/uuid.py` — the encoder/decoder. For example a `Uuid` class with
12
+ `shorten(value)`, `unshorten(value)`, `is_short_id(value)`, plus a module-level
13
+ `resolve_id(raw) -> uuid.UUID | None` accepting a short ID or a full UUID string.
14
+ Base-62 alphabet is `0-9`, `A-Z`, `a-z`; short IDs are exactly 22 chars.
15
+ - `app/converters.py` — `ShortIDConverter(werkzeug.routing.BaseConverter)`:
16
+ `to_python` decodes a short ID (or full UUID) to a `uuid.UUID`; `to_url` shortens.
17
+ The app factory registers it under the key `sid`.
18
+ - `app/json_provider.py` — `ShortIDJSONProvider(flask.json.provider.DefaultJSONProvider)`
19
+ whose `default` serializes any `uuid.UUID` as a short ID. The app factory installs
20
+ it if present.
21
+
22
+ Consequences: route id params are declared `<sid:...>` and arrive as `uuid.UUID`;
23
+ returning `to_dict()` (or any `uuid.UUID`) already emits short IDs; resolve inbound
24
+ body ids with `resolve_id(...)` before querying.
25
+
26
+ ## Resources (SQLAlchemy models)
27
+
28
+ - Define each resource in `app/models/<resource>.py` as a class extending `BaseModel`
29
+ (from `app/models/base.py`), which already provides the UUID `id`,
30
+ `created_at`/`updated_at`, and `to_dict()`/`save()`/`delete()`/`find_by_id()`/`find_all()`.
31
+ Do not reimplement those.
32
+ - Import the db handle as `from app.extensions import db`. Columns are `db.Column(...)`
33
+ with explicit types and nullability.
34
+ - Foreign keys:
35
+ `db.Column(SAUuid(as_uuid=True), db.ForeignKey('<table>.id', ondelete='CASCADE'|'RESTRICT'), nullable=False)`
36
+ with `from sqlalchemy import Uuid as SAUuid`.
37
+ - Constraints/indexes go in `__table_args__` (e.g.
38
+ `db.UniqueConstraint('user_id', 'name', name='uix_user_project_name')`).
39
+ - `__tablename__` is the snake_case plural. New model modules are auto-imported by the
40
+ app factory.
41
+
42
+ ## Endpoints (Flask blueprints)
43
+
44
+ - One blueprint per resource in `app/routes/<resource>.py`, named `<resource>_bp`. The
45
+ app factory auto-registers every Blueprint found under `app/routes/` — do not edit it.
46
+ - Paths: `/accounts/<sid:account_id>/<resource>` and
47
+ `/accounts/<sid:account_id>/<resource>/<sid:<resource>_id>`.
48
+ - Return `jsonify(model.to_dict())` for an item, `jsonify([m.to_dict() for m in ...])`
49
+ for a collection; `201` on create.
50
+
51
+ ## Authorization
52
+
53
+ - Decorate every protected handler with `@auth_required` from `app/utils/decorators.py`
54
+ (currently a pass-through stub — keep it in place; do not add auth dependencies).
55
+
56
+ ## Errors & tests
57
+
58
+ - Use `error_response(message, status, code=None)` and `success_response(data, status)`
59
+ from `app/utils/responses.py`. Error shape: `{"error": {"message": ..., "code"?: ...}}`.
60
+ - Put tests in `tests/test_<resource>.py` using the `app` and `client` fixtures from
61
+ `tests/conftest.py` (in-memory SQLite, tables created per test). Assert returned ids
62
+ are 22-char short IDs.
63
+
64
+ ## House rules (this stack)
65
+
66
+ - Stay within the shipped stack: Flask, Flask-SQLAlchemy, SQLAlchemy, pytest. Add no new
67
+ dependencies unless a feature requires it. The app must run on SQLite with no external
68
+ services. Never modify the short-ID files once generated, `BaseModel`, the app
69
+ factory's registration logic, or `conftest.py`.
@@ -0,0 +1,58 @@
1
+ """Application factory.
2
+
3
+ Blueprints and models are auto-discovered: drop a new module under
4
+ ``app/routes/`` that defines a ``Blueprint`` (conventionally ``<name>_bp``) and a
5
+ model under ``app/models/`` and they are wired up automatically — no manual
6
+ registration edits required.
7
+ """
8
+ import importlib
9
+ import pkgutil
10
+
11
+ from flask import Flask, Blueprint
12
+
13
+ from app.extensions import db, migrate
14
+
15
+
16
+ def _import_submodules(package_name: str) -> None:
17
+ package = importlib.import_module(package_name)
18
+ for _, name, _ in pkgutil.iter_modules(package.__path__):
19
+ importlib.import_module(f"{package_name}.{name}")
20
+
21
+
22
+ def create_app(config_object: str = "config.Config") -> Flask:
23
+ app = Flask(__name__)
24
+ app.config.from_object(config_object)
25
+
26
+ # Wire the short-ID scheme (generated per the identifiers convention):
27
+ # UUIDs serialize as 22-char Base-62 on the wire, and <sid:...> route params
28
+ # arrive as uuid.UUID objects. Loaded defensively so the app still boots
29
+ # before that layer has been generated.
30
+ try:
31
+ from app.json_provider import ShortIDJSONProvider
32
+
33
+ app.json = ShortIDJSONProvider(app)
34
+ except Exception:
35
+ pass
36
+ try:
37
+ from app.converters import ShortIDConverter
38
+
39
+ app.url_map.converters["sid"] = ShortIDConverter
40
+ except Exception:
41
+ pass
42
+
43
+ db.init_app(app)
44
+ migrate.init_app(app, db)
45
+
46
+ # Populate SQLAlchemy metadata by importing every model module.
47
+ _import_submodules("app.models")
48
+
49
+ # Auto-register every Blueprint defined under app.routes.
50
+ import app.routes as routes_pkg
51
+
52
+ for _, name, _ in pkgutil.iter_modules(routes_pkg.__path__):
53
+ module = importlib.import_module(f"app.routes.{name}")
54
+ for attr in vars(module).values():
55
+ if isinstance(attr, Blueprint):
56
+ app.register_blueprint(attr)
57
+
58
+ return app
@@ -0,0 +1,6 @@
1
+ """Flask extensions, instantiated without an app (initialized in the factory)."""
2
+ from flask_sqlalchemy import SQLAlchemy
3
+ from flask_migrate import Migrate
4
+
5
+ db = SQLAlchemy()
6
+ migrate = Migrate()
File without changes
@@ -0,0 +1,59 @@
1
+ """Base model with common fields. All models must extend BaseModel."""
2
+ import uuid
3
+ from datetime import datetime, timezone
4
+
5
+ from sqlalchemy import select, Uuid as SAUuid
6
+
7
+ from app.extensions import db
8
+
9
+
10
+ class BaseModel(db.Model):
11
+ """Base model class with common fields.
12
+
13
+ Provides a Base-62 short-ID-friendly UUID primary key plus created_at /
14
+ updated_at timestamps, and small persistence helpers. The ``Uuid`` column
15
+ type is dialect-agnostic (native UUID on Postgres, CHAR(32) on SQLite).
16
+ """
17
+ __abstract__ = True
18
+
19
+ id = db.Column(SAUuid(as_uuid=True), primary_key=True, default=uuid.uuid4)
20
+ created_at = db.Column(
21
+ db.DateTime,
22
+ default=lambda: datetime.now(timezone.utc).replace(tzinfo=None),
23
+ nullable=False,
24
+ )
25
+ updated_at = db.Column(
26
+ db.DateTime,
27
+ default=lambda: datetime.now(timezone.utc).replace(tzinfo=None),
28
+ onupdate=lambda: datetime.now(timezone.utc).replace(tzinfo=None),
29
+ nullable=False,
30
+ )
31
+
32
+ def to_dict(self):
33
+ """Convert model instance to a dictionary (UUIDs serialize as short IDs)."""
34
+ return {
35
+ column.name: getattr(self, column.name)
36
+ for column in self.__table__.columns
37
+ }
38
+
39
+ def save(self):
40
+ """Persist the model instance."""
41
+ db.session.add(self)
42
+ db.session.commit()
43
+ return self
44
+
45
+ def delete(self):
46
+ """Delete the model instance."""
47
+ db.session.delete(self)
48
+ db.session.commit()
49
+ return self
50
+
51
+ @classmethod
52
+ def find_by_id(cls, id):
53
+ """Find a model instance by primary key."""
54
+ return db.session.get(cls, id)
55
+
56
+ @classmethod
57
+ def find_all(cls):
58
+ """Return all model instances."""
59
+ return db.session.execute(select(cls)).scalars().all()
File without changes
@@ -0,0 +1,9 @@
1
+ """Health check route (kept minimal; safe to keep as the app grows)."""
2
+ from flask import Blueprint, jsonify
3
+
4
+ health_bp = Blueprint("health", __name__)
5
+
6
+
7
+ @health_bp.route("/health", methods=["GET"])
8
+ def health():
9
+ return jsonify({"status": "ok"})
File without changes
@@ -0,0 +1,16 @@
1
+ """Route decorators.
2
+
3
+ ``auth_required`` is a pass-through stub so generated apps run out of the box
4
+ while preserving the authentication convention. Replace its body with real
5
+ authentication (JWT/session/Clerk) when wiring auth for production.
6
+ """
7
+ from functools import wraps
8
+
9
+
10
+ def auth_required(func):
11
+ """Authentication gate (stub — currently a pass-through)."""
12
+ @wraps(func)
13
+ def wrapper(*args, **kwargs):
14
+ return func(*args, **kwargs)
15
+
16
+ return wrapper
@@ -0,0 +1,15 @@
1
+ """Standard JSON response helpers. Use these for all route responses."""
2
+ from flask import jsonify
3
+
4
+
5
+ def error_response(message: str, status: int = 400, code: str | None = None):
6
+ """Return a standardized JSON error: {"error": {"message", "code"?}}."""
7
+ payload = {"error": {"message": message}}
8
+ if code:
9
+ payload["error"]["code"] = code
10
+ return jsonify(payload), status
11
+
12
+
13
+ def success_response(data, status: int = 200):
14
+ """Return a JSON success response."""
15
+ return jsonify(data), status
@@ -0,0 +1,10 @@
1
+ """Application configuration."""
2
+ import os
3
+
4
+
5
+ class Config:
6
+ """Base config. Defaults to a local SQLite database for zero-setup runs."""
7
+
8
+ SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL", "sqlite:///quill_app.db")
9
+ SQLALCHEMY_TRACK_MODIFICATIONS = False
10
+ SECRET_KEY = os.environ.get("SECRET_KEY", "dev-secret-change-me")
@@ -0,0 +1,5 @@
1
+ Flask>=3.0
2
+ Flask-SQLAlchemy>=3.1
3
+ Flask-Migrate>=4.0
4
+ SQLAlchemy>=2.0
5
+ pytest>=8.0
File without changes
@@ -0,0 +1,24 @@
1
+ """Shared pytest fixtures. Generated tests should use the `app` and `client` fixtures."""
2
+ import pytest
3
+
4
+ from app import create_app
5
+ from app.extensions import db as _db
6
+
7
+
8
+ @pytest.fixture
9
+ def app():
10
+ app = create_app()
11
+ app.config.update(
12
+ TESTING=True,
13
+ SQLALCHEMY_DATABASE_URI="sqlite:///:memory:",
14
+ )
15
+ with app.app_context():
16
+ _db.create_all()
17
+ yield app
18
+ _db.session.remove()
19
+ _db.drop_all()
20
+
21
+
22
+ @pytest.fixture
23
+ def client(app):
24
+ return app.test_client()
@@ -0,0 +1,11 @@
1
+ """Baseline smoke tests for the scaffold. Feature prompts add their own tests."""
2
+
3
+
4
+ def test_app_factory(app):
5
+ assert app is not None
6
+
7
+
8
+ def test_health(client):
9
+ resp = client.get("/health")
10
+ assert resp.status_code == 200
11
+ assert resp.get_json()["status"] == "ok"
@@ -0,0 +1,7 @@
1
+ """WSGI entrypoint. Run with: FLASK_APP=wsgi flask run"""
2
+ from app import create_app
3
+
4
+ app = create_app()
5
+
6
+ if __name__ == "__main__":
7
+ app.run(debug=True)