@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.
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +67 -0
- package/dist/index.js.map +1 -0
- package/dist/verify.d.ts +3 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +145 -0
- package/dist/verify.js.map +1 -0
- package/package.json +40 -0
- package/prompts/binding.md +69 -0
- package/templates/app/__init__.py +58 -0
- package/templates/app/extensions.py +6 -0
- package/templates/app/models/__init__.py +0 -0
- package/templates/app/models/base.py +59 -0
- package/templates/app/routes/__init__.py +0 -0
- package/templates/app/routes/health.py +9 -0
- package/templates/app/utils/__init__.py +0 -0
- package/templates/app/utils/decorators.py +16 -0
- package/templates/app/utils/responses.py +15 -0
- package/templates/config.py +10 -0
- package/templates/requirements.txt +5 -0
- package/templates/tests/__init__.py +0 -0
- package/templates/tests/conftest.py +24 -0
- package/templates/tests/test_smoke.py +11 -0
- package/templates/wsgi.py +7 -0
package/dist/index.d.ts
ADDED
|
@@ -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"}
|
package/dist/verify.d.ts
ADDED
|
@@ -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
|
|
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
|
|
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")
|
|
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"
|