fastapi-fullstack 0.2.2__py3-none-any.whl → 0.2.4__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.
- {fastapi_fullstack-0.2.2.dist-info → fastapi_fullstack-0.2.4.dist-info}/METADATA +2 -2
- {fastapi_fullstack-0.2.2.dist-info → fastapi_fullstack-0.2.4.dist-info}/RECORD +29 -17
- fastapi_gen/template/hooks/post_gen_project.py +2 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/.claude/commands/add-endpoint.md +40 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/.claude/commands/fix-issue.md +16 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/.claude/commands/review.md +31 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/.claude/rules/api-conventions.md +90 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/.claude/rules/architecture.md +109 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/.claude/rules/code-style.md +67 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/.claude/rules/exceptions-security.md +54 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/.claude/rules/frontend.md +27 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/.claude/rules/schemas-models.md +90 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/.claude/rules/testing.md +85 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/.claude/settings.json +17 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/.github/workflows/ci.yml +2 -2
- fastapi_gen/template/{{cookiecutter.project_slug}}/.gitignore +2 -4
- fastapi_gen/template/{{cookiecutter.project_slug}}/.gitlab-ci.yml +2 -2
- fastapi_gen/template/{{cookiecutter.project_slug}}/CLAUDE.md +110 -57
- fastapi_gen/template/{{cookiecutter.project_slug}}/Makefile +2 -2
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/.pre-commit-config.yaml +3 -7
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic/env.py +1 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/sanitize.py +151 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/webhook.py +69 -3
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/pyproject.toml +86 -62
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_ssrf.py +207 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docs/commands.md +1 -1
- {fastapi_fullstack-0.2.2.dist-info → fastapi_fullstack-0.2.4.dist-info}/WHEEL +0 -0
- {fastapi_fullstack-0.2.2.dist-info → fastapi_fullstack-0.2.4.dist-info}/entry_points.txt +0 -0
- {fastapi_fullstack-0.2.2.dist-info → fastapi_fullstack-0.2.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-fullstack
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Full-stack FastAPI + Next.js template generator with PydanticAI/LangChain agents, WebSocket streaming, 20+ enterprise integrations, and Logfire/LangSmith observability. Ship AI apps fast. CLI tool to generate production-ready FastAPI + Next.js projects with AI agents, auth, and observability.
|
|
5
5
|
Project-URL: Homepage, https://github.com/vstorm-co/full-stack-ai-agent-template
|
|
6
6
|
Project-URL: Documentation, https://github.com/vstorm-co/full-stack-ai-agent-template#readme
|
|
@@ -27,11 +27,11 @@ Requires-Dist: pydantic>=2.0.0
|
|
|
27
27
|
Requires-Dist: questionary>=2.0.0
|
|
28
28
|
Requires-Dist: rich>=13.0.0
|
|
29
29
|
Provides-Extra: dev
|
|
30
|
-
Requires-Dist: mypy>=1.13.0; extra == 'dev'
|
|
31
30
|
Requires-Dist: pre-commit>=3.0.0; extra == 'dev'
|
|
32
31
|
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
|
|
33
32
|
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
34
33
|
Requires-Dist: ruff>=0.8.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: ty>=0.0.29; extra == 'dev'
|
|
35
35
|
Provides-Extra: docs
|
|
36
36
|
Requires-Dist: mkdocs-material>=9.5.0; extra == 'docs'
|
|
37
37
|
Requires-Dist: mkdocs>=1.6.0; extra == 'docs'
|
|
@@ -5,26 +5,37 @@ fastapi_gen/generator.py,sha256=aEYbD6eC_3B1Y7sY54AJkEWrXx58zVaTQrXqCy5r_Ac,8995
|
|
|
5
5
|
fastapi_gen/prompts.py,sha256=jsSckM9hpoPH4CQvXaDkxya2JDswZ86bmLgaLttH4AU,32287
|
|
6
6
|
fastapi_gen/template/VARIABLES.md,sha256=rB2N6RCvlARxw9hcvNxq2kSpiMjS2iQzryThioyx14o,19156
|
|
7
7
|
fastapi_gen/template/cookiecutter.json,sha256=Pxh3yc-uegduhWf6jGxeFTujzfDOl5jGpnQR06e3VQ0,3783
|
|
8
|
-
fastapi_gen/template/hooks/post_gen_project.py,sha256=
|
|
8
|
+
fastapi_gen/template/hooks/post_gen_project.py,sha256=Ou0Il1SZ6QX31VLND8NurhGgFEbcACI0c4n3YXBaoRU,17431
|
|
9
9
|
fastapi_gen/template/{{cookiecutter.project_slug}}/.env.prod.example,sha256=AnG_jOM673bJhncXC1jeAegPzCOhcWWu1_eKFUleXNU,2370
|
|
10
|
-
fastapi_gen/template/{{cookiecutter.project_slug}}/.gitignore,sha256=
|
|
11
|
-
fastapi_gen/template/{{cookiecutter.project_slug}}/.gitlab-ci.yml,sha256
|
|
10
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/.gitignore,sha256=Iasc-ahiDOjxHyAFJSIqBqgi6RkF8-yBp_wS_70oKkA,1064
|
|
11
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/.gitlab-ci.yml,sha256=3ajZ4hIeWMcagAFOK7c54Ef66M6rqPBZP69BCfDWzQo,4833
|
|
12
12
|
fastapi_gen/template/{{cookiecutter.project_slug}}/AGENTS.md,sha256=hShNTKVXEvBDRMcRfIqjbJnf2kQ-qMSAGR6b24B5lQs,2592
|
|
13
|
-
fastapi_gen/template/{{cookiecutter.project_slug}}/CLAUDE.md,sha256=
|
|
14
|
-
fastapi_gen/template/{{cookiecutter.project_slug}}/Makefile,sha256=
|
|
13
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/CLAUDE.md,sha256=YaJpZ4CAHMnon0kC2-PvylCZpiSS3jIGZY4GVgjNnwc,7474
|
|
14
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/Makefile,sha256=QhL-Djz_rOpx6uxXhHMKfSp29JiQzy1jwBf3lw51G0M,12120
|
|
15
15
|
fastapi_gen/template/{{cookiecutter.project_slug}}/README.md,sha256=GFGxGKIk5mqKh40_j8s_ASdEyj2KHUljlbvFULAfQTM,7684
|
|
16
16
|
fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.dev.yml,sha256=GTiJ5IgP-YOO7xqVFheiE6unthkg9FZOyFH9JcddnoE,10626
|
|
17
17
|
fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.frontend.yml,sha256=KvYVb7P1vlimx2_klOy-qnsh93tn40RfpUXV2qeH6Sw,1262
|
|
18
18
|
fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.prod.yml,sha256=7aCdZ7NtJOXNnlnki_Wu0INqprbaC2MYLZz1d8vfrc4,22759
|
|
19
19
|
fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.yml,sha256=Yd8Q8nzESQi1QoURtK7oPeZWgHuYxpLevLkkA2-3NhM,11804
|
|
20
|
-
fastapi_gen/template/{{cookiecutter.project_slug}}/.
|
|
20
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/.claude/settings.json,sha256=i6XfBDlx6N1AXg4-MrjmIzPhxRXvQC9PRXJFZyfNs1Y,363
|
|
21
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/.claude/commands/add-endpoint.md,sha256=p0EsXDvW23v4uOSPRlwA84nVrB7_VVL_FnKFEE442AQ,1648
|
|
22
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/.claude/commands/fix-issue.md,sha256=3bcQwYzf_EXTit9nlkplUd0-G5T9-gGranogIDS_kjU,800
|
|
23
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/.claude/commands/review.md,sha256=McWKMjMa-tCq8h_3-tx-uTUW4HoNAjb-H3zX8BnDne8,1179
|
|
24
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/.claude/rules/api-conventions.md,sha256=FFNZrbOY6O2f65QOsyD-Oo2vDX60OEGsvdvK2uY49P4,2234
|
|
25
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/.claude/rules/architecture.md,sha256=fG6vyFlrRnlS4BrBdR8G9Q_j455hxw9KJXdGaj7gaPs,3684
|
|
26
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/.claude/rules/code-style.md,sha256=2lDRkb3hjHkZnU6LGBiHs4lCUgI7uzJ9Pwk60Qbs0gk,2163
|
|
27
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/.claude/rules/exceptions-security.md,sha256=sJNlPg9FsqUBpotj9deoQOvkuX83WiVUJyn7T8G-NJE,1903
|
|
28
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/.claude/rules/frontend.md,sha256=jj9Yao5VRhHXpvG7DwfW8FDI9m4TPv0x0M2VCjKrKUw,759
|
|
29
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/.claude/rules/schemas-models.md,sha256=fUuyb8A0Zlzu4wpEjh-qrwFBfstv6b5Jr_-wmqNlttc,2901
|
|
30
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/.claude/rules/testing.md,sha256=w-zM54qfLNSB3tCMxBuOp7jABHS6dG1s62SGvSSXlxU,2267
|
|
31
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/.github/workflows/ci.yml,sha256=qR2eyiMNEhPo3cZ4HCS1dskVLK5O6xIaqP_zBtd6FDo,4926
|
|
21
32
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/.dockerignore,sha256=_iRaYQqTvt8_2yhJZUp60PPaL5XLiUFg0OSa-J_9XIQ,523
|
|
22
33
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/.env.example,sha256=SqlXMIr3j441dR82tP34i45ArX7r6vM-U4JLphMvYm0,8102
|
|
23
|
-
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/.pre-commit-config.yaml,sha256=
|
|
34
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/.pre-commit-config.yaml,sha256=qnK25jUF_eaT8sSelAhcKQ5Y1Y2hY2UE7CcJ0KOVKro,680
|
|
24
35
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/Dockerfile,sha256=G5g7qCHBtMyZdhHC_BB2VfSa9ingMzoVVE1K4fah47g,1628
|
|
25
36
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic.ini,sha256=Ol5tRsSTj20-C-9YOyQV17oEPFhPRi0aY859ZNc3cUs,872
|
|
26
|
-
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/pyproject.toml,sha256=
|
|
27
|
-
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic/env.py,sha256=
|
|
37
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/pyproject.toml,sha256=hsKJiScIGdBAerqaeoUEnsn9jvEJGG-hAajrbta3s6I,9683
|
|
38
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic/env.py,sha256=EhduURmw65KT58RHdemFiM4ZgNxaPtVOkJtUcsmMRLk,2169
|
|
28
39
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic/script.py.mako,sha256=hSTffz_8YXK8N5IfhFpyXXXWIho_zWEzMgdzZzTQPGo,817
|
|
29
40
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic/versions/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
30
41
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/__init__.py,sha256=vNnuyVXC5oZ3yq46meWLNhkUYlMgod70TgvWyvgNhBk,68
|
|
@@ -75,7 +86,7 @@ fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/logging.py,s
|
|
|
75
86
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/middleware.py,sha256=23eFiIULqywo3o7TsHf1ozUSmzJHxqRJHGVxn-JMqM4,3487
|
|
76
87
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/oauth.py,sha256=aAv64WrGyX7WR6XXce2SJ2l02uxgtLemd11Ilqx4dkE,597
|
|
77
88
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/rate_limit.py,sha256=emmc2orBEIT6yfrZvy1jxdr52_eVawYAQJx9MiFsQKk,1689
|
|
78
|
-
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/sanitize.py,sha256
|
|
89
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/sanitize.py,sha256=-XQBTypMEjw0Llbx7bNlG-rLIMQ0OW7ySRL4N8cjFaU,12476
|
|
79
90
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/security.py,sha256=AqT0q6y1PmhyhZeV97Ujtc2PQx8EdUKMeAa2VC2sCQY,2769
|
|
80
91
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/__init__.py,sha256=y9by535cwbinmqOftskTk0iALAtckQfpjW-KmyzGwhw,362
|
|
81
92
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/base.py,sha256=MZ-vPlY08QOesm7p3aFP2e8kzrmCeSM1fjbyRDm-ppQ,2289
|
|
@@ -134,7 +145,7 @@ fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/rag_sync
|
|
|
134
145
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/session.py,sha256=n-ICXvEr2S2t_rUo8Za5WknZSs4YBYkZ24iH5jWVTN8,11343
|
|
135
146
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/sync_source.py,sha256=I_YO1A3HddSkSbiES26EfQl3xXqA4nrsezj95QGJdJ0,9533
|
|
136
147
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/user.py,sha256=c3suO57uIbZKvlInWX4V-K6lAKshZQqVaqQUG2BkUYc,15020
|
|
137
|
-
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/webhook.py,sha256=
|
|
148
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/webhook.py,sha256=tUi03HqHm9Rnn0AS2PI4VZ9uoZ1IOuxXFpIMxv67zqA,19825
|
|
138
149
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/__init__.py,sha256=RRfSOD2hWUqo_rYkOc9KaE5BgqK7UkRC--Ni-BjMC3Q,169
|
|
139
150
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/arq_app.py,sha256=KLSBUUj9BY4pYYrJOpg9RM87ybZdSR4eDk9PoikawCA,4470
|
|
140
151
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/celery_app.py,sha256=FHp_tRNfEfqfE5qMpH3m1TkUn69T94DwPdjUKx8MdGQ,1829
|
|
@@ -159,6 +170,7 @@ fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_repositori
|
|
|
159
170
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_security.py,sha256=6YJUtT9HqIkhcyAEmQSIiYNunNVC_hmsrfjscmdON-k,3752
|
|
160
171
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_services.py,sha256=c2-rY6E9FQpb8U2ZCkPH1NvYOCJ-FS3crXlVXHKEQzU,13373
|
|
161
172
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_services_conversation.py,sha256=20TKPPCR7TkESDSjdrqiInrXpt5_3oQAc2ABYHr3Mys,35054
|
|
173
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_ssrf.py,sha256=Wez24gGr2np3doP9w0IYW08tAUyO58FtSuzPvhpkGyw,6832
|
|
162
174
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_worker.py,sha256=wnbYBPQvyr3Rr10dvoV6hvlJL2ChCIl77F47LSu1P3U,2502
|
|
163
175
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/__init__.py,sha256=WV9U5TSWbUYHVA2F3CD7gfod4RdX8EM1s4ewEnwszr0,25
|
|
164
176
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_auth.py,sha256=Z_RWiwvQTx73F2z1XSYSHA-9UxcaLdKR8EBkzzyTs5A,7852
|
|
@@ -169,7 +181,7 @@ fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_openap
|
|
|
169
181
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_users.py,sha256=0-GIPfch0nBFUKJL18HAzGNWOzc9TO04aMOOnGOrMMA,7674
|
|
170
182
|
fastapi_gen/template/{{cookiecutter.project_slug}}/docs/adding_features.md,sha256=EbNBxbLkswwMwa-Z4IAd8TxF_GHTmkgwZSeyEqth_h0,6929
|
|
171
183
|
fastapi_gen/template/{{cookiecutter.project_slug}}/docs/architecture.md,sha256=vU-7KQQp03uQOQ_jJ8INe5gks7TiUFHZ4JgzokGQDFc,11594
|
|
172
|
-
fastapi_gen/template/{{cookiecutter.project_slug}}/docs/commands.md,sha256=
|
|
184
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/docs/commands.md,sha256=mFW8XvDrO14jW6kboCHjxYN0bZRt6c4h4UWp7_VPqho,11091
|
|
173
185
|
fastapi_gen/template/{{cookiecutter.project_slug}}/docs/configuration.md,sha256=hV9KZAXp9QVZBTMjUo3XJ49FVJSHmMJiXs-TD4xUGBg,17272
|
|
174
186
|
fastapi_gen/template/{{cookiecutter.project_slug}}/docs/file-processing.md,sha256=3BGOT7hUAsdQFDYMmkI9CRVgc1kmC8V2jVFw8oSP0gk,11081
|
|
175
187
|
fastapi_gen/template/{{cookiecutter.project_slug}}/docs/patterns.md,sha256=YUTXhwP745bmilI8EeWXUOxqqU9RUBij6KFLFaPgA44,5949
|
|
@@ -346,8 +358,8 @@ fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/secret.yaml,sha256
|
|
|
346
358
|
fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/service.yaml,sha256=o3bCXguj8E5aQ0H1o3cBD3z_iZ_RA_Pmx1bc0sVzK28,682
|
|
347
359
|
fastapi_gen/template/{{cookiecutter.project_slug}}/nginx/nginx.conf,sha256=gJWUMMqNvr9TwNZOcc_WPPGZvdAlRUH3jLhbllOb_YY,7359
|
|
348
360
|
fastapi_gen/template/{{cookiecutter.project_slug}}/nginx/ssl/.gitkeep,sha256=o_WQQOgVGOLttT48x0EXSC4BbbDFemApFpmwhybcVUU,617
|
|
349
|
-
fastapi_fullstack-0.2.
|
|
350
|
-
fastapi_fullstack-0.2.
|
|
351
|
-
fastapi_fullstack-0.2.
|
|
352
|
-
fastapi_fullstack-0.2.
|
|
353
|
-
fastapi_fullstack-0.2.
|
|
361
|
+
fastapi_fullstack-0.2.4.dist-info/METADATA,sha256=RpJhZdB8NXiWtay8qfakChDNvtiGzPpRjzPOGvdOXNk,43013
|
|
362
|
+
fastapi_fullstack-0.2.4.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
363
|
+
fastapi_fullstack-0.2.4.dist-info/entry_points.txt,sha256=s9JXrISZp8LMYJGeVofOAd1wPTzpq-jwjSgSf4hWzjs,59
|
|
364
|
+
fastapi_fullstack-0.2.4.dist-info/licenses/LICENSE,sha256=bL4JuK_rcA8y__Gg7PEuTs3g2Qf6VvSz2w2Jajd6nVU,1063
|
|
365
|
+
fastapi_fullstack-0.2.4.dist-info/RECORD,,
|
|
@@ -263,6 +263,8 @@ if not use_frontend:
|
|
|
263
263
|
if os.path.exists(frontend_dir):
|
|
264
264
|
shutil.rmtree(frontend_dir)
|
|
265
265
|
print("Removed frontend/ directory (frontend not enabled)")
|
|
266
|
+
# Remove frontend-specific Claude rule
|
|
267
|
+
remove_file(os.path.join(os.getcwd(), ".claude", "rules", "frontend.md"))
|
|
266
268
|
|
|
267
269
|
|
|
268
270
|
# Remove .env files if generate_env is false
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Scaffold a new API endpoint with full layering
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Create a new API endpoint: $ARGUMENTS
|
|
6
|
+
|
|
7
|
+
Follow the project's layered architecture. Create files in this order:
|
|
8
|
+
|
|
9
|
+
1. **Schema** (`backend/app/schemas/<entity>.py`):
|
|
10
|
+
- Inherit `BaseSchema` (and `TimestampSchema` for Read)
|
|
11
|
+
- Create `*Create`, `*Update`, `*Read`, `*List` models
|
|
12
|
+
- Use `Field()` with constraints, `EmailStr` where applicable
|
|
13
|
+
|
|
14
|
+
2. **DB Model** (`backend/app/db/models/<entity>.py`):
|
|
15
|
+
- Inherit `Base, TimestampMixin`
|
|
16
|
+
- Use `Mapped[type]` + `mapped_column()`
|
|
17
|
+
- Add `__repr__`, relationships with `cascade="all, delete-orphan"`
|
|
18
|
+
|
|
19
|
+
3. **Repository** (`backend/app/repositories/<entity>_repo.py`):
|
|
20
|
+
- Stateless async functions: `get_by_id`, `get_multi`, `create`, `update`, `delete`
|
|
21
|
+
- Use `db.flush()` + `db.refresh()`, keyword-only args after `db`
|
|
22
|
+
|
|
23
|
+
4. **Service** (`backend/app/services/<entity>.py`):
|
|
24
|
+
- Class with `__init__(self, db: AsyncSession)`
|
|
25
|
+
- Raise `NotFoundError`, `AlreadyExistsError` as appropriate
|
|
26
|
+
|
|
27
|
+
5. **DI** (`backend/app/api/deps.py`):
|
|
28
|
+
- Add factory function and `Annotated` alias: `EntitySvc = Annotated[EntityService, Depends(get_entity_service)]`
|
|
29
|
+
|
|
30
|
+
6. **Route** (`backend/app/api/routes/v1/<entity>.py`):
|
|
31
|
+
- CRUD: GET list, GET by id, POST (201), PATCH, DELETE (204)
|
|
32
|
+
- Use DI aliases, `response_model`, `-> Any` return type
|
|
33
|
+
|
|
34
|
+
7. **Register** router in `backend/app/api/routes/v1/__init__.py`
|
|
35
|
+
|
|
36
|
+
8. **Migration**: `cd backend && uv run alembic revision --autogenerate -m "Add <entity> table"`
|
|
37
|
+
|
|
38
|
+
9. **Test** (`backend/tests/`): mirror source structure
|
|
39
|
+
|
|
40
|
+
10. Lint: `cd backend && uv run ruff check . --fix && uv run ruff format .`
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Investigate and fix an issue
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Fix the issue: $ARGUMENTS
|
|
6
|
+
|
|
7
|
+
1. **Understand** — search the codebase for relevant code, read the files, understand current behavior
|
|
8
|
+
2. **Reproduce** — if possible, identify a test case or request that triggers the issue
|
|
9
|
+
3. **Root cause** — trace through Routes → Services → Repositories to find where the bug originates
|
|
10
|
+
4. **Fix** — implement the fix following project conventions:
|
|
11
|
+
- Domain exceptions in services (not HTTP errors)
|
|
12
|
+
- `db.flush()` in repositories (not `commit`)
|
|
13
|
+
- Type hints on all changed signatures
|
|
14
|
+
5. **Test** — run `cd backend && uv run pytest` to verify no regressions
|
|
15
|
+
6. **Lint** — run `cd backend && uv run ruff check . --fix && uv run ruff format .`
|
|
16
|
+
7. **Summary** — explain what was changed and why
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Review code changes against project conventions
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Review all staged and unstaged changes in the current branch.
|
|
6
|
+
|
|
7
|
+
For each changed file, verify:
|
|
8
|
+
|
|
9
|
+
**Architecture:**
|
|
10
|
+
- Routes only call services, never repositories
|
|
11
|
+
- Services raise domain exceptions (NotFoundError, AlreadyExistsError, etc.), not HTTP exceptions
|
|
12
|
+
- Repositories use `db.flush()` + `db.refresh()`, never `db.commit()`
|
|
13
|
+
- DI uses Annotated aliases from `deps.py` (CurrentUser, *Svc), not raw `Depends()` in signatures
|
|
14
|
+
|
|
15
|
+
**Schemas & Types:**
|
|
16
|
+
- Separate Create/Update/Read/List Pydantic models
|
|
17
|
+
- Type hints on all function signatures (params + return)
|
|
18
|
+
- Modern syntax: `str | None` not `Optional[str]`
|
|
19
|
+
- Route return type is `-> Any`
|
|
20
|
+
|
|
21
|
+
**Code Quality:**
|
|
22
|
+
- No debug code (print, commented-out code, TODO without issue reference)
|
|
23
|
+
- No security issues (SQL injection, exposed secrets, missing auth)
|
|
24
|
+
- Consistent naming (snake_case functions, PascalCase classes)
|
|
25
|
+
- Imports ordered: stdlib → third-party → local
|
|
26
|
+
|
|
27
|
+
**Validation:**
|
|
28
|
+
1. Run `cd backend && uv run ruff check .`
|
|
29
|
+
2. Run `cd backend && uv run pytest` (if test files changed)
|
|
30
|
+
|
|
31
|
+
Provide findings with specific file:line references and suggest fixes.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: API design, REST conventions, auth, pagination, response format
|
|
3
|
+
globs: ["backend/app/api/**/*.py"]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# API Conventions
|
|
7
|
+
|
|
8
|
+
## Route Structure
|
|
9
|
+
|
|
10
|
+
- All routes under `/api/v1/` prefix
|
|
11
|
+
- One file per domain entity in `api/routes/v1/`
|
|
12
|
+
- Use `APIRouter()` with tags
|
|
13
|
+
|
|
14
|
+
## HTTP Methods & Status Codes
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
# GET — read
|
|
18
|
+
@router.get("/{id}", response_model=EntityRead)
|
|
19
|
+
|
|
20
|
+
# GET list — paginated
|
|
21
|
+
@router.get("", response_model=EntityList)
|
|
22
|
+
|
|
23
|
+
# POST — create (201)
|
|
24
|
+
@router.post("", response_model=EntityRead, status_code=status.HTTP_201_CREATED)
|
|
25
|
+
|
|
26
|
+
# PATCH — partial update
|
|
27
|
+
@router.patch("/{id}", response_model=EntityRead)
|
|
28
|
+
|
|
29
|
+
# DELETE — no content (204)
|
|
30
|
+
@router.delete("/{id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Pagination
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
@router.get("", response_model=ConversationList)
|
|
37
|
+
async def list_items(
|
|
38
|
+
service: ConversationSvc,
|
|
39
|
+
user: CurrentUser,
|
|
40
|
+
skip: int = Query(0, ge=0, description="Items to skip"),
|
|
41
|
+
limit: int = Query(50, ge=1, le=100, description="Max items to return"),
|
|
42
|
+
) -> Any:
|
|
43
|
+
items, total = await service.list(user_id=user.id, skip=skip, limit=limit)
|
|
44
|
+
return ConversationList(items=items, total=total)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Authentication
|
|
48
|
+
|
|
49
|
+
- `CurrentUser` — JWT Bearer token (any authenticated user)
|
|
50
|
+
- `CurrentAdmin` — JWT + admin role check via `RoleChecker`
|
|
51
|
+
- `ValidAPIKey` — API key from header (service-to-service)
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
# Protected endpoint
|
|
55
|
+
async def get_profile(user: CurrentUser) -> Any: ...
|
|
56
|
+
|
|
57
|
+
# Admin-only endpoint
|
|
58
|
+
async def delete_user(user: CurrentAdmin) -> Any: ...
|
|
59
|
+
|
|
60
|
+
# API key endpoint
|
|
61
|
+
async def webhook_callback(api_key: ValidAPIKey) -> Any: ...
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Response Format
|
|
65
|
+
|
|
66
|
+
All route handlers return `-> Any`. The `response_model` parameter handles serialization.
|
|
67
|
+
|
|
68
|
+
Error responses follow this JSON structure:
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"error": {
|
|
72
|
+
"code": "NOT_FOUND",
|
|
73
|
+
"message": "User not found",
|
|
74
|
+
"details": {"user_id": "..."}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## File Upload
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
@router.post("/me/avatar", response_model=UserRead)
|
|
83
|
+
async def upload_avatar(
|
|
84
|
+
file: UploadFile = File(...),
|
|
85
|
+
user: CurrentUser,
|
|
86
|
+
service: UserSvc,
|
|
87
|
+
) -> Any:
|
|
88
|
+
data = await file.read()
|
|
89
|
+
return await service.update_avatar(user.id, data, file.filename or "avatar.jpg")
|
|
90
|
+
```
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Layered architecture patterns — Routes, Services, Repositories, DI
|
|
3
|
+
globs: ["backend/app/**/*.py"]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Architecture
|
|
7
|
+
|
|
8
|
+
## Layered Architecture: Routes → Services → Repositories
|
|
9
|
+
|
|
10
|
+
Routes NEVER import or call repositories directly. Always go through a service.
|
|
11
|
+
|
|
12
|
+
## Repositories (`app/repositories/`)
|
|
13
|
+
|
|
14
|
+
Pure data access — stateless functions (not classes):
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
async def get_by_id(db: AsyncSession, entity_id: UUID) -> Entity | None:
|
|
18
|
+
result = await db.execute(select(Entity).where(Entity.id == entity_id))
|
|
19
|
+
return result.scalar_one_or_none()
|
|
20
|
+
|
|
21
|
+
async def create(db: AsyncSession, *, field1: str, field2: str) -> Entity:
|
|
22
|
+
entity = Entity(field1=field1, field2=field2)
|
|
23
|
+
db.add(entity)
|
|
24
|
+
await db.flush()
|
|
25
|
+
await db.refresh(entity)
|
|
26
|
+
return entity
|
|
27
|
+
|
|
28
|
+
async def update(db: AsyncSession, *, db_entity: Entity, update_data: dict[str, Any]) -> Entity:
|
|
29
|
+
for field, value in update_data.items():
|
|
30
|
+
setattr(db_entity, field, value)
|
|
31
|
+
await db.flush()
|
|
32
|
+
await db.refresh(db_entity)
|
|
33
|
+
return db_entity
|
|
34
|
+
|
|
35
|
+
async def delete(db: AsyncSession, entity_id: UUID) -> Entity | None:
|
|
36
|
+
entity = await get_by_id(db, entity_id)
|
|
37
|
+
if entity:
|
|
38
|
+
await db.delete(entity)
|
|
39
|
+
await db.flush()
|
|
40
|
+
return entity
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Rules:
|
|
44
|
+
- ALWAYS `db.flush()` + `db.refresh()`, NEVER `db.commit()` — session auto-commits in `get_db_session`
|
|
45
|
+
- Use keyword-only args after `db`: `create(db, *, email: str, name: str)`
|
|
46
|
+
- Return the entity (or None for get/delete), never return IDs or dicts
|
|
47
|
+
- Functions are async for PostgreSQL/MongoDB, sync for SQLite
|
|
48
|
+
|
|
49
|
+
## Services (`app/services/`)
|
|
50
|
+
|
|
51
|
+
Business logic — class-based with DB session:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
class UserService:
|
|
55
|
+
def __init__(self, db: AsyncSession):
|
|
56
|
+
self.db = db
|
|
57
|
+
|
|
58
|
+
async def get_by_id(self, user_id: UUID) -> User:
|
|
59
|
+
user = await user_repo.get_by_id(self.db, user_id)
|
|
60
|
+
if not user:
|
|
61
|
+
raise NotFoundError(message="User not found", details={"user_id": user_id})
|
|
62
|
+
return user
|
|
63
|
+
|
|
64
|
+
async def create(self, data: UserCreate) -> User:
|
|
65
|
+
existing = await user_repo.get_by_email(self.db, data.email)
|
|
66
|
+
if existing:
|
|
67
|
+
raise AlreadyExistsError(message="Email already registered", details={"email": data.email})
|
|
68
|
+
hashed_password = get_password_hash(data.password)
|
|
69
|
+
return await user_repo.create(self.db, email=data.email, hashed_password=hashed_password)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Rules:
|
|
73
|
+
- Raise domain exceptions, NEVER return error codes or None for "not found"
|
|
74
|
+
- Services call repo functions, never build raw queries
|
|
75
|
+
- One service per domain entity
|
|
76
|
+
|
|
77
|
+
## Dependency Injection (`app/api/deps.py`)
|
|
78
|
+
|
|
79
|
+
Use `Annotated` type aliases — never raw `Depends()` in route signatures:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
DBSession = Annotated[AsyncSession, Depends(get_db_session)]
|
|
83
|
+
UserSvc = Annotated[UserService, Depends(get_user_service)]
|
|
84
|
+
CurrentUser = Annotated[User, Depends(get_current_user)]
|
|
85
|
+
CurrentAdmin = Annotated[User, Depends(RoleChecker(UserRole.ADMIN))]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Service factories take `DBSession` and return service instances:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
def get_user_service(db: DBSession) -> UserService:
|
|
92
|
+
return UserService(db)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Routes (`app/api/routes/v1/`)
|
|
96
|
+
|
|
97
|
+
HTTP layer only — validate, delegate, return:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
@router.get("/{user_id}", response_model=UserRead)
|
|
101
|
+
async def get_user(user_id: UUID, service: UserSvc, user: CurrentUser) -> Any:
|
|
102
|
+
return await service.get_by_id(user_id)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Rules:
|
|
106
|
+
- Return type is always `-> Any` (response_model handles serialization)
|
|
107
|
+
- Use `status_code=status.HTTP_201_CREATED` for POST, `HTTP_204_NO_CONTENT` for DELETE
|
|
108
|
+
- DELETE endpoints: `response_model=None`
|
|
109
|
+
- Pagination: `skip: int = Query(0, ge=0)`, `limit: int = Query(50, ge=1, le=100)`
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Code style, formatting, naming, imports, and type hints
|
|
3
|
+
globs: ["backend/**/*.py", "*.py"]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Code Style
|
|
7
|
+
|
|
8
|
+
## Formatting
|
|
9
|
+
|
|
10
|
+
- Use `ruff` for linting and formatting: `ruff check . --fix && ruff format .`
|
|
11
|
+
- Line length: 120 characters
|
|
12
|
+
|
|
13
|
+
## Type Hints
|
|
14
|
+
|
|
15
|
+
- Type hints on ALL function signatures — parameters and return types
|
|
16
|
+
- Use modern syntax: `str | None` not `Optional[str]`, `list[User]` not `List[User]`
|
|
17
|
+
- Use `Annotated[Type, Depends(...)]` for DI (defined as aliases in `deps.py`)
|
|
18
|
+
- Use `dict[str, Any]` for generic dicts
|
|
19
|
+
- Use `Literal["value1", "value2"]` for string enums in schemas
|
|
20
|
+
- Use `TYPE_CHECKING` block for circular import resolution:
|
|
21
|
+
```python
|
|
22
|
+
from typing import TYPE_CHECKING
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from app.db.models.session import Session
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Naming
|
|
28
|
+
|
|
29
|
+
| Element | Convention | Example |
|
|
30
|
+
|---------|-----------|---------|
|
|
31
|
+
| Files | snake_case | `user_repo.py`, `conversation_service.py` |
|
|
32
|
+
| Classes | PascalCase | `UserService`, `ConversationRead` |
|
|
33
|
+
| Functions/variables | snake_case | `get_by_id`, `user_service` |
|
|
34
|
+
| Constants | UPPER_CASE | `DEFAULT_SYSTEM_PROMPT` |
|
|
35
|
+
| Private | _leading_underscore | `_create_agent` |
|
|
36
|
+
| DB tables | snake_case plural | `users`, `conversations` |
|
|
37
|
+
| API URLs | kebab-case | `/api/v1/conversations` |
|
|
38
|
+
|
|
39
|
+
## Imports — strictly ordered, separated by blank lines
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
# 1. Standard library
|
|
43
|
+
import logging
|
|
44
|
+
from collections.abc import AsyncGenerator
|
|
45
|
+
from datetime import UTC, datetime
|
|
46
|
+
from typing import Annotated, Any
|
|
47
|
+
from uuid import UUID
|
|
48
|
+
|
|
49
|
+
# 2. Third-party
|
|
50
|
+
from fastapi import APIRouter, Depends, Query, status
|
|
51
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
52
|
+
from sqlalchemy import select
|
|
53
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
54
|
+
|
|
55
|
+
# 3. Local application
|
|
56
|
+
from app.api.deps import CurrentUser, UserSvc
|
|
57
|
+
from app.core.exceptions import NotFoundError
|
|
58
|
+
from app.schemas.user import UserCreate, UserRead
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Other Conventions
|
|
62
|
+
|
|
63
|
+
- `datetime.now(UTC)` not `datetime.utcnow()`
|
|
64
|
+
- `secrets.compare_digest()` for constant-time comparisons
|
|
65
|
+
- `__repr__` on all DB models
|
|
66
|
+
- Async for PostgreSQL/MongoDB I/O, sync for SQLite
|
|
67
|
+
- Keyword-only args in repo functions after `db` parameter
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Exception handling patterns and security conventions
|
|
3
|
+
globs: ["backend/app/core/**/*.py", "backend/app/services/**/*.py"]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Exceptions & Security
|
|
7
|
+
|
|
8
|
+
## Domain Exceptions (`app/core/exceptions.py`)
|
|
9
|
+
|
|
10
|
+
All extend `AppException`. Always pass `message` and `details`:
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
raise NotFoundError(message="User not found", details={"user_id": str(user_id)})
|
|
14
|
+
raise AlreadyExistsError(message="Email already registered", details={"email": email})
|
|
15
|
+
raise AuthenticationError(message="Invalid or expired token")
|
|
16
|
+
raise AuthorizationError(message="Role 'admin' required for this action")
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Exception handlers in `api/exception_handlers.py` automatically:
|
|
20
|
+
- Map to HTTP status codes
|
|
21
|
+
- Log with structured context (path, method, error code)
|
|
22
|
+
- Return consistent JSON error format
|
|
23
|
+
- Add `WWW-Authenticate: Bearer` header on 401
|
|
24
|
+
|
|
25
|
+
## Security Patterns
|
|
26
|
+
|
|
27
|
+
JWT auth (`core/security.py`):
|
|
28
|
+
- `create_access_token(subject)` / `create_refresh_token(subject)` — encode with `jwt.encode()`
|
|
29
|
+
- `verify_token(token)` → `dict | None` — decode with `jwt.decode()`
|
|
30
|
+
- Token payload: `{"exp": ..., "sub": user_id, "type": "access"|"refresh"}`
|
|
31
|
+
|
|
32
|
+
Password hashing:
|
|
33
|
+
- `get_password_hash(password)` — bcrypt
|
|
34
|
+
- `verify_password(plain, hashed)` — bcrypt `checkpw`
|
|
35
|
+
- NEVER store plain passwords
|
|
36
|
+
|
|
37
|
+
API keys:
|
|
38
|
+
- `secrets.compare_digest()` for constant-time comparison
|
|
39
|
+
- `APIKeyHeader(name=settings.API_KEY_HEADER, auto_error=False)`
|
|
40
|
+
|
|
41
|
+
## Role-Based Access Control
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
class RoleChecker:
|
|
45
|
+
def __init__(self, required_role: UserRole) -> None:
|
|
46
|
+
self.required_role = required_role
|
|
47
|
+
|
|
48
|
+
async def __call__(self, user: Annotated[User, Depends(get_current_user)]) -> User:
|
|
49
|
+
if not user.has_role(self.required_role):
|
|
50
|
+
raise AuthorizationError(message=f"Role '{self.required_role.value}' required")
|
|
51
|
+
return user
|
|
52
|
+
|
|
53
|
+
CurrentAdmin = Annotated[User, Depends(RoleChecker(UserRole.ADMIN))]
|
|
54
|
+
```
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Frontend conventions for Next.js
|
|
3
|
+
globs: ["frontend/**/*.ts", "frontend/**/*.tsx", "frontend/**/*.css"]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Frontend Conventions
|
|
7
|
+
|
|
8
|
+
## Stack
|
|
9
|
+
|
|
10
|
+
- Next.js 15 with App Router
|
|
11
|
+
- TypeScript strict mode
|
|
12
|
+
- Tailwind CSS for styling
|
|
13
|
+
- i18n support built-in
|
|
14
|
+
|
|
15
|
+
## Structure
|
|
16
|
+
|
|
17
|
+
- Pages in `frontend/src/app/` following Next.js App Router conventions
|
|
18
|
+
- Reusable components in `frontend/src/components/`
|
|
19
|
+
- API client functions in `frontend/src/lib/`
|
|
20
|
+
- Types in `frontend/src/types/`
|
|
21
|
+
|
|
22
|
+
## Conventions
|
|
23
|
+
|
|
24
|
+
- Use `"use client"` directive only when component needs client-side interactivity
|
|
25
|
+
- Prefer Server Components by default
|
|
26
|
+
- Use `fetch` with proper error handling for API calls
|
|
27
|
+
- Keep components small and focused — extract when a component exceeds ~100 lines
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Pydantic schema patterns and SQLAlchemy model conventions
|
|
3
|
+
globs: ["backend/app/schemas/**/*.py", "backend/app/db/models/**/*.py", "backend/app/db/base.py"]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Schemas & Models
|
|
7
|
+
|
|
8
|
+
## Pydantic Schemas (`app/schemas/`)
|
|
9
|
+
|
|
10
|
+
Base schema with shared config:
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
class BaseSchema(BaseModel):
|
|
14
|
+
model_config = ConfigDict(
|
|
15
|
+
from_attributes=True,
|
|
16
|
+
populate_by_name=True,
|
|
17
|
+
str_strip_whitespace=True,
|
|
18
|
+
)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Separate models per operation:
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
class UserCreate(BaseSchema):
|
|
25
|
+
email: EmailStr = Field(max_length=255)
|
|
26
|
+
password: str = Field(min_length=8, max_length=128)
|
|
27
|
+
full_name: str | None = Field(default=None, max_length=255)
|
|
28
|
+
|
|
29
|
+
class UserUpdate(BaseSchema):
|
|
30
|
+
email: EmailStr | None = Field(default=None, max_length=255)
|
|
31
|
+
password: str | None = Field(default=None, min_length=8, max_length=128)
|
|
32
|
+
full_name: str | None = Field(default=None, max_length=255)
|
|
33
|
+
is_active: bool | None = None
|
|
34
|
+
|
|
35
|
+
class UserRead(BaseSchema, TimestampSchema):
|
|
36
|
+
id: UUID
|
|
37
|
+
email: EmailStr
|
|
38
|
+
full_name: str | None = None
|
|
39
|
+
role: UserRole = UserRole.USER
|
|
40
|
+
avatar_url: str | None = None
|
|
41
|
+
|
|
42
|
+
class UserList(BaseSchema):
|
|
43
|
+
items: list[UserRead]
|
|
44
|
+
total: int
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Rules:
|
|
48
|
+
- `*Create` — required fields for creation, with `Field()` constraints
|
|
49
|
+
- `*Update` — all fields optional (`type | None = None`)
|
|
50
|
+
- `*Read` — includes `id` and timestamps, inherits `TimestampSchema`
|
|
51
|
+
- `*List` — `items` list + `total` count
|
|
52
|
+
- Use `@field_validator` for complex deserialization (e.g., JSON string → dict)
|
|
53
|
+
|
|
54
|
+
## SQLAlchemy Models (`app/db/models/`)
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
class User(Base, TimestampMixin):
|
|
58
|
+
__tablename__ = "users"
|
|
59
|
+
|
|
60
|
+
id: Mapped[UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
61
|
+
email: Mapped[str] = mapped_column(String(255), unique=True, index=True, nullable=False)
|
|
62
|
+
hashed_password: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
63
|
+
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
|
64
|
+
|
|
65
|
+
conversations: Mapped[list["Conversation"]] = relationship(
|
|
66
|
+
"Conversation", back_populates="user", cascade="all, delete-orphan"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def __repr__(self) -> str:
|
|
70
|
+
return f"<User(id={self.id}, email={self.email})>"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Rules:
|
|
74
|
+
- Always inherit `Base` and `TimestampMixin` (provides `created_at`, `updated_at`)
|
|
75
|
+
- Use `Mapped[type]` with `mapped_column()` for all columns
|
|
76
|
+
- ForeignKey with `ondelete="CASCADE"` for parent references
|
|
77
|
+
- Always define `__repr__`
|
|
78
|
+
- Naming convention in `Base.metadata`: `{table}_{col}_key`, `{table}_{col}_fkey`, etc.
|
|
79
|
+
|
|
80
|
+
## TimestampMixin
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
class TimestampMixin:
|
|
84
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
85
|
+
DateTime(timezone=True), server_default=func.now(), nullable=False
|
|
86
|
+
)
|
|
87
|
+
updated_at: Mapped[datetime | None] = mapped_column(
|
|
88
|
+
DateTime(timezone=True), onupdate=func.now(), nullable=True
|
|
89
|
+
)
|
|
90
|
+
```
|