@ripla/godd-mcp 0.1.2-beta-20260312091403
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/README.md +412 -0
- package/dist/godd.cjs +465 -0
- package/dist/godd.js +34845 -0
- package/dist/index.js +224 -0
- package/notes-api/.env.example +29 -0
- package/notes-api/README.md +65 -0
- package/notes-api/alembic/README +1 -0
- package/notes-api/alembic/env.py +62 -0
- package/notes-api/alembic/script.py.mako +28 -0
- package/notes-api/alembic/versions/001_initial_notes_tables.py +71 -0
- package/notes-api/alembic.ini +149 -0
- package/notes-api/app/__init__.py +1 -0
- package/notes-api/app/config.py +90 -0
- package/notes-api/app/database.py +31 -0
- package/notes-api/app/main.py +55 -0
- package/notes-api/app/models/__init__.py +17 -0
- package/notes-api/app/models/audit.py +38 -0
- package/notes-api/app/models/base.py +38 -0
- package/notes-api/app/models/session.py +32 -0
- package/notes-api/app/models/setting.py +16 -0
- package/notes-api/app/models/user.py +38 -0
- package/notes-api/app/routers/__init__.py +1 -0
- package/notes-api/app/routers/auth.py +205 -0
- package/notes-api/app/routers/files.py +76 -0
- package/notes-api/app/routers/pr.py +92 -0
- package/notes-api/app/routers/settings.py +44 -0
- package/notes-api/app/routers/tree.py +22 -0
- package/notes-api/app/routers/users.py +145 -0
- package/notes-api/app/schemas/__init__.py +1 -0
- package/notes-api/app/schemas/auth.py +45 -0
- package/notes-api/app/schemas/user.py +41 -0
- package/notes-api/app/security.py +94 -0
- package/notes-api/app/services/__init__.py +1 -0
- package/notes-api/app/services/audit.py +25 -0
- package/notes-api/app/services/business_date.py +38 -0
- package/notes-api/app/services/github.py +140 -0
- package/notes-api/app/services/github_pr.py +120 -0
- package/notes-api/app/services/pr_chain.py +225 -0
- package/notes-api/app/startup.py +66 -0
- package/notes-api/docker/Dockerfile +14 -0
- package/notes-api/docker/Dockerfile.test +14 -0
- package/notes-api/pyproject.toml +53 -0
- package/notes-api/tests/conftest.py +21 -0
- package/notes-api/tests/test_business_date.py +32 -0
- package/notes-api/tests/test_health.py +11 -0
- package/notes-api/uv.lock +1012 -0
- package/notes-app/README.md +30 -0
- package/notes-app/docker/Dockerfile.prod +12 -0
- package/notes-app/docker/Dockerfile.test +12 -0
- package/notes-app/eslint.config.js +23 -0
- package/notes-app/index.html +12 -0
- package/notes-app/nginx.conf +23 -0
- package/notes-app/package.json +37 -0
- package/notes-app/pnpm-lock.yaml +3124 -0
- package/notes-app/src/App.tsx +13 -0
- package/notes-app/src/main.tsx +13 -0
- package/notes-app/src/pages/LoginPage.test.tsx +15 -0
- package/notes-app/src/pages/LoginPage.tsx +10 -0
- package/notes-app/src/pages/MainPage.tsx +14 -0
- package/notes-app/src/styles/globals.css +1 -0
- package/notes-app/src/vite-env.d.ts +1 -0
- package/notes-app/tsconfig.json +24 -0
- package/notes-app/tsconfig.node.json +10 -0
- package/notes-app/vite.config.js +21 -0
- package/notes-app/vite.config.ts +22 -0
- package/notes-app/vitest.config.ts +16 -0
- package/package.json +58 -0
- package/stacks/django-vue.yaml +33 -0
- package/stacks/fastapi-react.yaml +160 -0
- package/stacks/flutter-firebase.yaml +58 -0
- package/stacks/nextjs-prisma.yaml +33 -0
- package/stacks/schema.json +144 -0
- package/templates/agents/architect.hbs +183 -0
- package/templates/agents/auto-documenter.hbs +36 -0
- package/templates/agents/backend-developer.hbs +175 -0
- package/templates/agents/code-reviewer.hbs +29 -0
- package/templates/agents/database-developer.hbs +52 -0
- package/templates/agents/debug-specialist.hbs +39 -0
- package/templates/agents/environment-setup.hbs +41 -0
- package/templates/agents/frontend-developer.hbs +45 -0
- package/templates/agents/integration-qa.hbs +26 -0
- package/templates/agents/performance-qa.hbs +24 -0
- package/templates/agents/pr-writer.hbs +53 -0
- package/templates/agents/quality-lead.hbs +26 -0
- package/templates/agents/requirements-analyst.hbs +26 -0
- package/templates/agents/security-analyst.hbs +21 -0
- package/templates/agents/security-reviewer.hbs +20 -0
- package/templates/agents/ssot-updater.hbs +45 -0
- package/templates/agents/technical-writer.hbs +28 -0
- package/templates/agents/unit-test-engineer.hbs +19 -0
- package/templates/agents/user-clone.hbs +43 -0
- package/templates/github-actions/aws/deploy-notes.yml.hbs +118 -0
- package/templates/github-actions/azure/deploy-notes.yml.hbs +78 -0
- package/templates/github-actions/gcp/deploy-notes.yml.hbs +76 -0
- package/templates/github-actions/vercel-railway/deploy-notes.yml.hbs +49 -0
- package/templates/mindsets/agent-stdio.hbs +51 -0
- package/templates/mindsets/dart.hbs +39 -0
- package/templates/mindsets/ddd-clean-architecture.hbs +52 -0
- package/templates/mindsets/electron.hbs +41 -0
- package/templates/mindsets/fastapi.hbs +40 -0
- package/templates/mindsets/flutter.hbs +56 -0
- package/templates/mindsets/kotlin.hbs +46 -0
- package/templates/mindsets/postgresql.hbs +39 -0
- package/templates/mindsets/python.hbs +39 -0
- package/templates/mindsets/react.hbs +51 -0
- package/templates/mindsets/swift.hbs +46 -0
- package/templates/mindsets/terraform.hbs +53 -0
- package/templates/mindsets/typescript.hbs +34 -0
- package/templates/mindsets/vite.hbs +37 -0
- package/templates/notes-compose.yml +79 -0
- package/templates/partials/cogito-geo.hbs +84 -0
- package/templates/partials/cogito-godd.hbs +22 -0
- package/templates/partials/cogito-grammar.hbs +105 -0
- package/templates/partials/cogito-particle.hbs +76 -0
- package/templates/partials/cogito-root-ai.hbs +79 -0
- package/templates/partials/cogito-root-arch.hbs +75 -0
- package/templates/partials/cogito-root-core.hbs +75 -0
- package/templates/partials/cogito-root-daily.hbs +58 -0
- package/templates/partials/cogito-root-dev.hbs +71 -0
- package/templates/partials/cogito-root-devops.hbs +71 -0
- package/templates/partials/cogito-root-framework.hbs +80 -0
- package/templates/partials/cogito-root-lang.hbs +74 -0
- package/templates/partials/godd-core.hbs +50 -0
- package/templates/partials/project-context.hbs +52 -0
- package/templates/partials/ssot-header.hbs +40 -0
- package/templates/partials/ssot-rules.hbs +30 -0
- package/templates/prompts/adr.hbs +39 -0
- package/templates/prompts/agent-dev-workflow.hbs +34 -0
- package/templates/prompts/analyze-report.hbs +36 -0
- package/templates/prompts/auto-documentation.hbs +37 -0
- package/templates/prompts/check.hbs +52 -0
- package/templates/prompts/commit.hbs +96 -0
- package/templates/prompts/dev.hbs +127 -0
- package/templates/prompts/docs-init.hbs +95 -0
- package/templates/prompts/docs-update.hbs +51 -0
- package/templates/prompts/git-workflow.hbs +125 -0
- package/templates/prompts/impact-analysis.hbs +43 -0
- package/templates/prompts/install.hbs +40 -0
- package/templates/prompts/new-branch.hbs +61 -0
- package/templates/prompts/notes-deploy.hbs +72 -0
- package/templates/prompts/pr-analyze.hbs +44 -0
- package/templates/prompts/pr-create.hbs +125 -0
- package/templates/prompts/push-execute.hbs +83 -0
- package/templates/prompts/push.hbs +24 -0
- package/templates/prompts/release-notes.hbs +38 -0
- package/templates/prompts/requirements-to-business-flow.hbs +29 -0
- package/templates/prompts/requirements-to-tickets.hbs +26 -0
- package/templates/prompts/review.hbs +126 -0
- package/templates/prompts/setup.hbs +247 -0
- package/templates/prompts/spec.hbs +35 -0
- package/templates/prompts/specification-to-tickets.hbs +24 -0
- package/templates/prompts/submit.hbs +143 -0
- package/templates/prompts/sync.hbs +63 -0
- package/templates/prompts/test-plan.hbs +56 -0
- package/templates/terraform/aws/alb.tf.hbs +58 -0
- package/templates/terraform/aws/backend.tf.hbs +10 -0
- package/templates/terraform/aws/cloudfront.tf.hbs +96 -0
- package/templates/terraform/aws/ecr.tf.hbs +30 -0
- package/templates/terraform/aws/ecs.tf.hbs +136 -0
- package/templates/terraform/aws/iam.tf.hbs +88 -0
- package/templates/terraform/aws/local.tf.hbs +34 -0
- package/templates/terraform/aws/output.tf.hbs +49 -0
- package/templates/terraform/aws/provider.tf.hbs +26 -0
- package/templates/terraform/aws/rds.tf.hbs +38 -0
- package/templates/terraform/aws/s3.tf.hbs +34 -0
- package/templates/terraform/aws/secrets.tf.hbs +38 -0
- package/templates/terraform/aws/security_groups.tf.hbs +72 -0
- package/templates/terraform/aws/vpc.tf.hbs +63 -0
- package/templates/terraform/azure/backend.tf.hbs +8 -0
- package/templates/terraform/azure/iam.tf.hbs +36 -0
- package/templates/terraform/azure/main.tf.hbs +199 -0
- package/templates/terraform/azure/output.tf.hbs +19 -0
- package/templates/terraform/azure/provider.tf.hbs +19 -0
- package/templates/terraform/gcp/backend.tf.hbs +6 -0
- package/templates/terraform/gcp/iam.tf.hbs +47 -0
- package/templates/terraform/gcp/main.tf.hbs +171 -0
- package/templates/terraform/gcp/output.tf.hbs +15 -0
- package/templates/terraform/gcp/provider.tf.hbs +19 -0
- package/templates/terraform/vercel-railway/main.tf.hbs +64 -0
- package/templates/terraform/vercel-railway/output.tf.hbs +11 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""PR chain: save to branch, create PR, discard."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import and_, select
|
|
8
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
9
|
+
|
|
10
|
+
from app.models import NotesEditingSession, NotesUser
|
|
11
|
+
from app.services.business_date import get_business_date
|
|
12
|
+
from app.services.github_pr import (
|
|
13
|
+
close_pr,
|
|
14
|
+
commit_file,
|
|
15
|
+
create_branch,
|
|
16
|
+
create_pr,
|
|
17
|
+
delete_branch,
|
|
18
|
+
find_open_auto_prs,
|
|
19
|
+
get_branch_sha,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def save_to_working_branch(
|
|
24
|
+
db: AsyncSession,
|
|
25
|
+
file_path: str,
|
|
26
|
+
content: str,
|
|
27
|
+
message: str,
|
|
28
|
+
existing_sha: str | None,
|
|
29
|
+
user_info: dict,
|
|
30
|
+
config: dict,
|
|
31
|
+
) -> dict:
|
|
32
|
+
"""Save file to user's business date branch."""
|
|
33
|
+
from app.models import NotesAuditLog
|
|
34
|
+
from app.services.audit import audit_log
|
|
35
|
+
|
|
36
|
+
business_date = await get_business_date(db)
|
|
37
|
+
branch_name = f"docs/{user_info['username']}/{business_date}"
|
|
38
|
+
|
|
39
|
+
result = await db.execute(
|
|
40
|
+
select(NotesEditingSession)
|
|
41
|
+
.where(
|
|
42
|
+
and_(
|
|
43
|
+
NotesEditingSession.user_id == UUID(user_info["id"]),
|
|
44
|
+
NotesEditingSession.business_date == business_date,
|
|
45
|
+
NotesEditingSession.status == "active",
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
.limit(1)
|
|
49
|
+
)
|
|
50
|
+
session = result.scalar_one_or_none()
|
|
51
|
+
if not session:
|
|
52
|
+
base_sha = await get_branch_sha(config["branch"], config)
|
|
53
|
+
await create_branch(branch_name, base_sha, config)
|
|
54
|
+
session = NotesEditingSession(
|
|
55
|
+
user_id=UUID(user_info["id"]),
|
|
56
|
+
branch_name=branch_name,
|
|
57
|
+
business_date=business_date,
|
|
58
|
+
status="active",
|
|
59
|
+
commit_count=0,
|
|
60
|
+
changed_files=[],
|
|
61
|
+
)
|
|
62
|
+
db.add(session)
|
|
63
|
+
await db.flush()
|
|
64
|
+
await db.refresh(session)
|
|
65
|
+
|
|
66
|
+
result = await commit_file(branch_name, file_path, content, message, existing_sha, config)
|
|
67
|
+
session.commit_count = (session.commit_count or 0) + 1
|
|
68
|
+
changed = session.changed_files or []
|
|
69
|
+
if file_path not in changed:
|
|
70
|
+
session.changed_files = changed + [file_path]
|
|
71
|
+
await db.flush()
|
|
72
|
+
|
|
73
|
+
await audit_log(db, UUID(user_info["id"]), "save", detail={"filePath": file_path, "branch": branch_name})
|
|
74
|
+
|
|
75
|
+
return {"branch": branch_name, "sha": result["sha"], "commitCount": session.commit_count}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
async def create_ready_pr(db: AsyncSession, user_id: UUID, config: dict) -> dict:
|
|
79
|
+
"""Create PR from user's business date branch."""
|
|
80
|
+
from app.services.audit import audit_log
|
|
81
|
+
from app.services.business_date import get_business_date
|
|
82
|
+
|
|
83
|
+
business_date = await get_business_date(db)
|
|
84
|
+
result = await db.execute(
|
|
85
|
+
select(NotesEditingSession)
|
|
86
|
+
.where(
|
|
87
|
+
and_(
|
|
88
|
+
NotesEditingSession.user_id == user_id,
|
|
89
|
+
NotesEditingSession.business_date == business_date,
|
|
90
|
+
NotesEditingSession.status == "active",
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
.limit(1)
|
|
94
|
+
)
|
|
95
|
+
session = result.scalar_one_or_none()
|
|
96
|
+
if not session:
|
|
97
|
+
raise ValueError("No active session found for today")
|
|
98
|
+
|
|
99
|
+
if session.pr_number:
|
|
100
|
+
prs = await find_open_auto_prs(config)
|
|
101
|
+
for p in prs:
|
|
102
|
+
if p["number"] == session.pr_number:
|
|
103
|
+
return p
|
|
104
|
+
|
|
105
|
+
user_result = await db.execute(select(NotesUser).where(NotesUser.id == user_id))
|
|
106
|
+
user = user_result.scalar_one_or_none()
|
|
107
|
+
display_name = user.display_name if user else "Unknown"
|
|
108
|
+
username = user.username if user else "unknown"
|
|
109
|
+
changed_files = session.changed_files or []
|
|
110
|
+
body = "\n".join(
|
|
111
|
+
[
|
|
112
|
+
"## ripla Notes 自動生成 PR",
|
|
113
|
+
"",
|
|
114
|
+
f"- **作成者**: {display_name} (@{username})",
|
|
115
|
+
f"- **営業日**: {business_date}",
|
|
116
|
+
f"- **ブランチ**: `{session.branch_name}`",
|
|
117
|
+
f"- **コミット数**: {session.commit_count}",
|
|
118
|
+
"",
|
|
119
|
+
"### 変更ファイル",
|
|
120
|
+
*[f"- `{f}`" for f in changed_files],
|
|
121
|
+
]
|
|
122
|
+
)
|
|
123
|
+
pr = await create_pr(
|
|
124
|
+
f"[docs] {display_name} のドキュメント更新 ({business_date})",
|
|
125
|
+
session.branch_name,
|
|
126
|
+
config["branch"],
|
|
127
|
+
body,
|
|
128
|
+
["docs-auto", f"author:{username}"],
|
|
129
|
+
config,
|
|
130
|
+
)
|
|
131
|
+
session.pr_number = pr["number"]
|
|
132
|
+
session.pr_url = pr.get("html_url", "")
|
|
133
|
+
session.status = "pr_created"
|
|
134
|
+
await db.flush()
|
|
135
|
+
await audit_log(db, user_id, "pr_create", detail={"prNumber": pr["number"], "branch": session.branch_name})
|
|
136
|
+
return pr
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
async def discard_changes(db: AsyncSession, user_id: UUID, config: dict) -> None:
|
|
140
|
+
"""Discard changes: close PR, delete branch."""
|
|
141
|
+
from app.services.business_date import get_business_date
|
|
142
|
+
|
|
143
|
+
business_date = await get_business_date(db)
|
|
144
|
+
result = await db.execute(
|
|
145
|
+
select(NotesEditingSession)
|
|
146
|
+
.where(
|
|
147
|
+
and_(
|
|
148
|
+
NotesEditingSession.user_id == user_id,
|
|
149
|
+
NotesEditingSession.business_date == business_date,
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
.limit(1)
|
|
153
|
+
)
|
|
154
|
+
session = result.scalar_one_or_none()
|
|
155
|
+
if not session:
|
|
156
|
+
raise ValueError("No session found for today")
|
|
157
|
+
if session.pr_number:
|
|
158
|
+
await close_pr(session.pr_number, config)
|
|
159
|
+
await delete_branch(session.branch_name, config)
|
|
160
|
+
session.status = "discarded"
|
|
161
|
+
await db.flush()
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
async def get_session_for_user(db: AsyncSession, user_id: UUID) -> dict | None:
|
|
165
|
+
"""Get active session for user + business date."""
|
|
166
|
+
from app.services.business_date import get_business_date
|
|
167
|
+
|
|
168
|
+
business_date = await get_business_date(db)
|
|
169
|
+
result = await db.execute(
|
|
170
|
+
select(NotesEditingSession)
|
|
171
|
+
.where(
|
|
172
|
+
and_(
|
|
173
|
+
NotesEditingSession.user_id == user_id,
|
|
174
|
+
NotesEditingSession.business_date == business_date,
|
|
175
|
+
NotesEditingSession.status == "active",
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
.limit(1)
|
|
179
|
+
)
|
|
180
|
+
session = result.scalar_one_or_none()
|
|
181
|
+
if not session:
|
|
182
|
+
return None
|
|
183
|
+
return {
|
|
184
|
+
"id": str(session.id),
|
|
185
|
+
"branchName": session.branch_name,
|
|
186
|
+
"prNumber": session.pr_number,
|
|
187
|
+
"prUrl": session.pr_url,
|
|
188
|
+
"commitCount": session.commit_count,
|
|
189
|
+
"businessDate": session.business_date,
|
|
190
|
+
"status": session.status,
|
|
191
|
+
"changedFiles": session.changed_files or [],
|
|
192
|
+
"createdAt": session.created_at.isoformat() if session.created_at else None,
|
|
193
|
+
"updatedAt": session.updated_at.isoformat() if session.updated_at else None,
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
async def get_all_active_sessions(db: AsyncSession) -> list:
|
|
198
|
+
"""Get all active sessions with user info."""
|
|
199
|
+
from app.services.business_date import get_business_date
|
|
200
|
+
|
|
201
|
+
business_date = await get_business_date(db)
|
|
202
|
+
result = await db.execute(
|
|
203
|
+
select(NotesEditingSession, NotesUser.username, NotesUser.display_name)
|
|
204
|
+
.join(NotesUser, NotesEditingSession.user_id == NotesUser.id)
|
|
205
|
+
.where(NotesEditingSession.status == "active")
|
|
206
|
+
.order_by(NotesEditingSession.updated_at)
|
|
207
|
+
)
|
|
208
|
+
rows = result.all()
|
|
209
|
+
return [
|
|
210
|
+
{
|
|
211
|
+
"id": str(sess.id),
|
|
212
|
+
"branchName": sess.branch_name,
|
|
213
|
+
"prNumber": sess.pr_number,
|
|
214
|
+
"prUrl": sess.pr_url,
|
|
215
|
+
"commitCount": sess.commit_count,
|
|
216
|
+
"businessDate": sess.business_date,
|
|
217
|
+
"status": sess.status,
|
|
218
|
+
"changedFiles": sess.changed_files or [],
|
|
219
|
+
"createdAt": sess.created_at.isoformat() if sess.created_at else None,
|
|
220
|
+
"updatedAt": sess.updated_at.isoformat() if sess.updated_at else None,
|
|
221
|
+
"username": username,
|
|
222
|
+
"displayName": display_name,
|
|
223
|
+
}
|
|
224
|
+
for sess, username, display_name in rows
|
|
225
|
+
]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Startup: ensure tables and seed data."""
|
|
2
|
+
|
|
3
|
+
from sqlalchemy import select
|
|
4
|
+
from sqlalchemy.dialects.postgresql import insert
|
|
5
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
6
|
+
|
|
7
|
+
from app.config import settings
|
|
8
|
+
from app.database import async_session_factory
|
|
9
|
+
from app.models import NotesSetting, NotesUser
|
|
10
|
+
from app.security import hash_password
|
|
11
|
+
|
|
12
|
+
DEFAULT_SETTINGS = [
|
|
13
|
+
{"key": "business_day_start_hour", "value": 6},
|
|
14
|
+
{"key": "timezone", "value": "Asia/Tokyo"},
|
|
15
|
+
{"key": "pr_idle_timeout_sec", "value": 300},
|
|
16
|
+
{"key": "max_login_attempts", "value": 5},
|
|
17
|
+
{"key": "lockout_duration_min", "value": 15},
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def ensure_tables_and_seed() -> None:
|
|
22
|
+
"""Create tables and seed admin user + default settings."""
|
|
23
|
+
from app.database import engine
|
|
24
|
+
from app.models import ( # noqa: F401
|
|
25
|
+
NotesAuditLog,
|
|
26
|
+
NotesEditingSession,
|
|
27
|
+
NotesSetting,
|
|
28
|
+
NotesUser,
|
|
29
|
+
)
|
|
30
|
+
from app.models.base import Base
|
|
31
|
+
|
|
32
|
+
async with engine.begin() as conn:
|
|
33
|
+
await conn.run_sync(Base.metadata.create_all)
|
|
34
|
+
|
|
35
|
+
async with async_session_factory() as session:
|
|
36
|
+
await _seed_admin(session)
|
|
37
|
+
await _seed_settings(session)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def _seed_admin(session: AsyncSession) -> None:
|
|
41
|
+
"""Create admin user if not exists."""
|
|
42
|
+
result = await session.execute(
|
|
43
|
+
select(NotesUser).where(NotesUser.username == settings.notes_admin_username)
|
|
44
|
+
)
|
|
45
|
+
if result.scalar_one_or_none() is None:
|
|
46
|
+
admin = NotesUser(
|
|
47
|
+
username=settings.notes_admin_username,
|
|
48
|
+
display_name="Administrator",
|
|
49
|
+
password_hash=hash_password(settings.notes_admin_password),
|
|
50
|
+
role="admin",
|
|
51
|
+
)
|
|
52
|
+
session.add(admin)
|
|
53
|
+
await session.commit()
|
|
54
|
+
print(f'[seed] Admin user "{settings.notes_admin_username}" created')
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def _seed_settings(session: AsyncSession) -> None:
|
|
58
|
+
"""Insert default settings (on conflict do nothing)."""
|
|
59
|
+
for item in DEFAULT_SETTINGS:
|
|
60
|
+
stmt = insert(NotesSetting).values(
|
|
61
|
+
key=item["key"],
|
|
62
|
+
value=item["value"],
|
|
63
|
+
).on_conflict_do_nothing(index_elements=["key"])
|
|
64
|
+
await session.execute(stmt)
|
|
65
|
+
await session.commit()
|
|
66
|
+
print("[seed] Default settings ensured")
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
FROM python:3.12-slim
|
|
2
|
+
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
|
|
5
|
+
RUN apt-get update && apt-get install -y --no-install-recommends libpq-dev gcc && rm -rf /var/lib/apt/lists/*
|
|
6
|
+
|
|
7
|
+
RUN pip install uv
|
|
8
|
+
|
|
9
|
+
COPY . .
|
|
10
|
+
|
|
11
|
+
RUN uv pip install --system .
|
|
12
|
+
|
|
13
|
+
EXPOSE 3100
|
|
14
|
+
CMD ["sh", "-c", "uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-3100}"]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
FROM python:3.12-slim
|
|
2
|
+
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
|
|
5
|
+
RUN apt-get update && apt-get install -y --no-install-recommends libpq-dev gcc && rm -rf /var/lib/apt/lists/*
|
|
6
|
+
|
|
7
|
+
RUN pip install --no-cache-dir uv
|
|
8
|
+
|
|
9
|
+
COPY pyproject.toml uv.lock* ./
|
|
10
|
+
RUN uv pip install --system ".[dev]"
|
|
11
|
+
|
|
12
|
+
COPY . .
|
|
13
|
+
|
|
14
|
+
CMD ["sh", "-c", "ruff check . && pytest -q"]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "notes-api"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "ripla Notes API - FastAPI backend for docs viewing/editing"
|
|
5
|
+
requires-python = ">=3.12"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"fastapi>=0.115.0",
|
|
8
|
+
"uvicorn[standard]>=0.34.0",
|
|
9
|
+
"sqlalchemy[asyncio]>=2.0.36",
|
|
10
|
+
"asyncpg>=0.30.0",
|
|
11
|
+
"alembic>=1.14.0",
|
|
12
|
+
"pydantic>=2.10.0",
|
|
13
|
+
"pydantic-settings>=2.7.0",
|
|
14
|
+
"python-multipart>=0.0.18",
|
|
15
|
+
"psycopg2-binary>=2.9.0",
|
|
16
|
+
"pyjwt>=2.9.0",
|
|
17
|
+
"bcrypt>=4.2.0",
|
|
18
|
+
"httpx>=0.28.0",
|
|
19
|
+
"tzdata>=2024.1; sys_platform == 'win32'",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.optional-dependencies]
|
|
23
|
+
dev = [
|
|
24
|
+
"pytest>=8.0.0",
|
|
25
|
+
"pytest-asyncio>=0.25.0",
|
|
26
|
+
"httpx>=0.28.0",
|
|
27
|
+
"ruff>=0.9.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[build-system]
|
|
31
|
+
requires = ["hatchling"]
|
|
32
|
+
build-backend = "hatchling.build"
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.wheel]
|
|
35
|
+
packages = ["app"]
|
|
36
|
+
|
|
37
|
+
[tool.ruff]
|
|
38
|
+
target-version = "py312"
|
|
39
|
+
line-length = 100
|
|
40
|
+
|
|
41
|
+
[tool.ruff.lint]
|
|
42
|
+
select = ["E", "F", "W", "I", "N", "UP", "B", "A", "SIM", "TCH"]
|
|
43
|
+
ignore = [
|
|
44
|
+
"B008",
|
|
45
|
+
"A002",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
[tool.ruff.lint.per-file-ignores]
|
|
49
|
+
"scripts/*.py" = ["E501"]
|
|
50
|
+
"alembic/**/*.py" = ["E501"]
|
|
51
|
+
|
|
52
|
+
[tool.pytest.ini_options]
|
|
53
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Pytest fixtures for notes-api."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from httpx import ASGITransport, AsyncClient
|
|
7
|
+
|
|
8
|
+
# Skip DB-dependent startup during tests
|
|
9
|
+
os.environ["TESTING"] = "true"
|
|
10
|
+
|
|
11
|
+
from app.main import app # noqa: E402
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.fixture
|
|
15
|
+
async def client():
|
|
16
|
+
"""Async test client."""
|
|
17
|
+
async with AsyncClient(
|
|
18
|
+
transport=ASGITransport(app=app),
|
|
19
|
+
base_url="http://test",
|
|
20
|
+
) as ac:
|
|
21
|
+
yield ac
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Test business date calculation."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from zoneinfo import ZoneInfo
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from app.services.business_date import compute_business_date
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_compute_business_date_before_start():
|
|
12
|
+
"""Before start_hour counts as previous day."""
|
|
13
|
+
tz = ZoneInfo("Asia/Tokyo")
|
|
14
|
+
now = datetime(2026, 2, 27, 5, 0, 0, tzinfo=tz)
|
|
15
|
+
result = compute_business_date(now, 6, "Asia/Tokyo")
|
|
16
|
+
assert result == "2026-02-26"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_compute_business_date_after_start():
|
|
20
|
+
"""After start_hour counts as same day."""
|
|
21
|
+
tz = ZoneInfo("Asia/Tokyo")
|
|
22
|
+
now = datetime(2026, 2, 27, 10, 0, 0, tzinfo=tz)
|
|
23
|
+
result = compute_business_date(now, 6, "Asia/Tokyo")
|
|
24
|
+
assert result == "2026-02-27"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_compute_business_date_at_boundary():
|
|
28
|
+
"""At exactly start_hour counts as same day."""
|
|
29
|
+
tz = ZoneInfo("Asia/Tokyo")
|
|
30
|
+
now = datetime(2026, 2, 27, 6, 0, 0, tzinfo=tz)
|
|
31
|
+
result = compute_business_date(now, 6, "Asia/Tokyo")
|
|
32
|
+
assert result == "2026-02-27"
|