gtm-admin 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. gtm_admin-0.1.0/.env.example +3 -0
  2. gtm_admin-0.1.0/.gitignore +43 -0
  3. gtm_admin-0.1.0/LICENSE +21 -0
  4. gtm_admin-0.1.0/Makefile +10 -0
  5. gtm_admin-0.1.0/PKG-INFO +130 -0
  6. gtm_admin-0.1.0/README.md +80 -0
  7. gtm_admin-0.1.0/alembic/env.py +45 -0
  8. gtm_admin-0.1.0/alembic/script.py.mako +26 -0
  9. gtm_admin-0.1.0/alembic.ini +38 -0
  10. gtm_admin-0.1.0/app/__init__.py +0 -0
  11. gtm_admin-0.1.0/app/auth.py +21 -0
  12. gtm_admin-0.1.0/app/config.py +16 -0
  13. gtm_admin-0.1.0/app/database.py +16 -0
  14. gtm_admin-0.1.0/app/deps.py +33 -0
  15. gtm_admin-0.1.0/app/main.py +8 -0
  16. gtm_admin-0.1.0/app/models/__init__.py +4 -0
  17. gtm_admin-0.1.0/app/models/user.py +22 -0
  18. gtm_admin-0.1.0/app/models/workflow_run.py +42 -0
  19. gtm_admin-0.1.0/app/routers/__init__.py +0 -0
  20. gtm_admin-0.1.0/app/routers/auth.py +31 -0
  21. gtm_admin-0.1.0/app/routers/workflow_runs.py +77 -0
  22. gtm_admin-0.1.0/hatch_build.py +31 -0
  23. gtm_admin-0.1.0/pyproject.toml +46 -0
  24. gtm_admin-0.1.0/src/gtm_admin/__init__.py +3 -0
  25. gtm_admin-0.1.0/src/gtm_admin/auth.py +34 -0
  26. gtm_admin-0.1.0/src/gtm_admin/core.py +66 -0
  27. gtm_admin-0.1.0/src/gtm_admin/database.py +46 -0
  28. gtm_admin-0.1.0/src/gtm_admin/decorator.py +58 -0
  29. gtm_admin-0.1.0/src/gtm_admin/deps.py +27 -0
  30. gtm_admin-0.1.0/src/gtm_admin/migrations/env.py +52 -0
  31. gtm_admin-0.1.0/src/gtm_admin/migrations/script.py.mako +26 -0
  32. gtm_admin-0.1.0/src/gtm_admin/migrations/versions/.gitkeep +0 -0
  33. gtm_admin-0.1.0/src/gtm_admin/models.py +27 -0
  34. gtm_admin-0.1.0/src/gtm_admin/router.py +77 -0
  35. gtm_admin-0.1.0/src/gtm_admin/static/assets/index-BTDQOZNI.js +485 -0
  36. gtm_admin-0.1.0/src/gtm_admin/static/assets/index-fyDHVEth.css +1 -0
  37. gtm_admin-0.1.0/src/gtm_admin/static/index.html +14 -0
  38. gtm_admin-0.1.0/uv.lock +1680 -0
@@ -0,0 +1,3 @@
1
+ DATABASE_URL=postgresql://user:password@localhost:5432/gtm_admin
2
+ SECRET_KEY=change-me-to-a-random-secret-key
3
+ ACCESS_TOKEN_EXPIRE_MINUTES=30
@@ -0,0 +1,43 @@
1
+ # Python / uv
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.pyo
5
+ *.pyd
6
+ .Python
7
+ *.egg
8
+ *.egg-info/
9
+ dist/
10
+ build/
11
+ .eggs/
12
+ .venv/
13
+ venv/
14
+ .uv/
15
+ .uv-cache/
16
+ *.so
17
+ .env
18
+ .env.*
19
+ !.env.example
20
+
21
+ # Generated static files — produced at build time, never committed
22
+ api/src/gtm_admin/static/
23
+
24
+ # Node / Vite
25
+ node_modules/
26
+ dist/
27
+ dist-ssr/
28
+ *.local
29
+ .pnpm-store/
30
+
31
+ # Logs
32
+ npm-debug.log*
33
+ yarn-debug.log*
34
+ yarn-error.log*
35
+ pnpm-debug.log*
36
+
37
+ # Editor / OS
38
+ .DS_Store
39
+ .vscode/
40
+ .idea/
41
+ *.swp
42
+ *.swo
43
+ Thumbs.db
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 GTM Admin Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,10 @@
1
+ .PHONY: init dev
2
+
3
+ init:
4
+ cp -n .env.example .env || true
5
+ uv sync
6
+ alembic revision --autogenerate -m "initial"
7
+ alembic upgrade head
8
+
9
+ dev:
10
+ uv run uvicorn app.main:app --reload
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.4
2
+ Name: gtm-admin
3
+ Version: 0.1.0
4
+ Summary: Workflow monitoring and management for FastAPI apps
5
+ License: MIT License
6
+
7
+ Copyright (c) 2026 GTM Admin Contributors
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
26
+ License-File: LICENSE
27
+ Classifier: Framework :: FastAPI
28
+ Classifier: License :: OSI Approved :: MIT License
29
+ Classifier: Programming Language :: Python :: 3
30
+ Classifier: Programming Language :: Python :: 3.11
31
+ Classifier: Programming Language :: Python :: 3.12
32
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
33
+ Requires-Python: >=3.11
34
+ Requires-Dist: alembic>=1.14.0
35
+ Requires-Dist: asyncpg>=0.30.0
36
+ Requires-Dist: fastapi>=0.115.0
37
+ Requires-Dist: httpx>=0.27.0
38
+ Requires-Dist: passlib[bcrypt]>=1.7.4
39
+ Requires-Dist: python-jose[cryptography]>=3.3.0
40
+ Requires-Dist: python-multipart>=0.0.12
41
+ Requires-Dist: sqlmodel>=0.0.22
42
+ Requires-Dist: uvicorn[standard]>=0.32.0
43
+ Provides-Extra: dev
44
+ Requires-Dist: build>=1.0.0; extra == 'dev'
45
+ Requires-Dist: hatchling>=1.25.0; extra == 'dev'
46
+ Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
47
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
48
+ Requires-Dist: twine>=5.0.0; extra == 'dev'
49
+ Description-Content-Type: text/markdown
50
+
51
+ # gtm-admin
52
+
53
+ Workflow monitoring and management for FastAPI apps.
54
+
55
+ `gtm-admin` adds a self-hosted dashboard and run-tracking API to **any FastAPI application** with zero infrastructure setup. You write pure Python workflow functions — it handles capture, persistence, and visualization.
56
+
57
+ ## Install
58
+
59
+ ```bash
60
+ pip install gtm-admin
61
+ ```
62
+
63
+ ## Usage
64
+
65
+ ```python
66
+ import os
67
+ from fastapi import FastAPI
68
+ from gtm_admin import GTMAdmin
69
+
70
+ app = FastAPI()
71
+
72
+ gtm = GTMAdmin(
73
+ app,
74
+ db_url=os.environ["GTM_DB_URL"], # postgresql://... connection string
75
+ secret=os.environ["GTM_SECRET"], # random secret for JWT signing
76
+ )
77
+
78
+ @gtm.workflow
79
+ async def enrich_leads(inputs: dict):
80
+ """Each decorated function becomes a tracked workflow."""
81
+ results = call_some_api(inputs["leads"])
82
+ return {"enriched": results}
83
+ ```
84
+
85
+ This mounts:
86
+ - **`/admin`** — React dashboard SPA (run history, status, I/O inspection)
87
+ - **`/api/*`** — REST API consumed by the dashboard
88
+
89
+ ## Routes
90
+
91
+ | Path | Description |
92
+ |------|-------------|
93
+ | `/api/runs` | List all workflow runs |
94
+ | `/api/runs/{id}` | Run detail (inputs, outputs, timing, errors) |
95
+ | `/api/workflows` | List registered workflows |
96
+ | `/api/health` | Health check |
97
+ | `/admin/` | Dashboard SPA |
98
+
99
+ ## Deployment (Digital Ocean App Platform)
100
+
101
+ ```dockerfile
102
+ FROM python:3.11-slim
103
+ WORKDIR /app
104
+ COPY requirements.txt .
105
+ RUN pip install gtm-admin
106
+ RUN pip install -r requirements.txt
107
+ COPY . .
108
+ EXPOSE 8080
109
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]
110
+ ```
111
+
112
+ Set in DO console: `GTM_DB_URL` (Supabase/Neon), `GTM_SECRET`.
113
+
114
+ ## Development mode
115
+
116
+ In dev mode, `/admin/*` is proxied to a Vite HMR server instead of serving static files:
117
+
118
+ ```python
119
+ gtm = GTMAdmin(app, db_url=..., secret=..., dev=os.getenv("GTM_DEV") == "true")
120
+ ```
121
+
122
+ ## Requirements
123
+
124
+ - Python 3.11+
125
+ - PostgreSQL database (Supabase, Neon, or self-hosted)
126
+ - FastAPI app
127
+
128
+ ## License
129
+
130
+ MIT
@@ -0,0 +1,80 @@
1
+ # gtm-admin
2
+
3
+ Workflow monitoring and management for FastAPI apps.
4
+
5
+ `gtm-admin` adds a self-hosted dashboard and run-tracking API to **any FastAPI application** with zero infrastructure setup. You write pure Python workflow functions — it handles capture, persistence, and visualization.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install gtm-admin
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```python
16
+ import os
17
+ from fastapi import FastAPI
18
+ from gtm_admin import GTMAdmin
19
+
20
+ app = FastAPI()
21
+
22
+ gtm = GTMAdmin(
23
+ app,
24
+ db_url=os.environ["GTM_DB_URL"], # postgresql://... connection string
25
+ secret=os.environ["GTM_SECRET"], # random secret for JWT signing
26
+ )
27
+
28
+ @gtm.workflow
29
+ async def enrich_leads(inputs: dict):
30
+ """Each decorated function becomes a tracked workflow."""
31
+ results = call_some_api(inputs["leads"])
32
+ return {"enriched": results}
33
+ ```
34
+
35
+ This mounts:
36
+ - **`/admin`** — React dashboard SPA (run history, status, I/O inspection)
37
+ - **`/api/*`** — REST API consumed by the dashboard
38
+
39
+ ## Routes
40
+
41
+ | Path | Description |
42
+ |------|-------------|
43
+ | `/api/runs` | List all workflow runs |
44
+ | `/api/runs/{id}` | Run detail (inputs, outputs, timing, errors) |
45
+ | `/api/workflows` | List registered workflows |
46
+ | `/api/health` | Health check |
47
+ | `/admin/` | Dashboard SPA |
48
+
49
+ ## Deployment (Digital Ocean App Platform)
50
+
51
+ ```dockerfile
52
+ FROM python:3.11-slim
53
+ WORKDIR /app
54
+ COPY requirements.txt .
55
+ RUN pip install gtm-admin
56
+ RUN pip install -r requirements.txt
57
+ COPY . .
58
+ EXPOSE 8080
59
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]
60
+ ```
61
+
62
+ Set in DO console: `GTM_DB_URL` (Supabase/Neon), `GTM_SECRET`.
63
+
64
+ ## Development mode
65
+
66
+ In dev mode, `/admin/*` is proxied to a Vite HMR server instead of serving static files:
67
+
68
+ ```python
69
+ gtm = GTMAdmin(app, db_url=..., secret=..., dev=os.getenv("GTM_DEV") == "true")
70
+ ```
71
+
72
+ ## Requirements
73
+
74
+ - Python 3.11+
75
+ - PostgreSQL database (Supabase, Neon, or self-hosted)
76
+ - FastAPI app
77
+
78
+ ## License
79
+
80
+ MIT
@@ -0,0 +1,45 @@
1
+ from logging.config import fileConfig
2
+
3
+ from alembic import context
4
+ from sqlalchemy import engine_from_config, pool
5
+ from sqlmodel import SQLModel
6
+
7
+ from app.config import settings
8
+ import app.models # noqa: F401 — ensure all models are registered
9
+
10
+ config = context.config
11
+ config.set_main_option("sqlalchemy.url", settings.database_url)
12
+
13
+ if config.config_file_name is not None:
14
+ fileConfig(config.config_file_name)
15
+
16
+ target_metadata = SQLModel.metadata
17
+
18
+
19
+ def run_migrations_offline() -> None:
20
+ context.configure(
21
+ url=settings.database_url,
22
+ target_metadata=target_metadata,
23
+ literal_binds=True,
24
+ dialect_opts={"paramstyle": "named"},
25
+ )
26
+ with context.begin_transaction():
27
+ context.run_migrations()
28
+
29
+
30
+ def run_migrations_online() -> None:
31
+ connectable = engine_from_config(
32
+ config.get_section(config.config_ini_section, {}),
33
+ prefix="sqlalchemy.",
34
+ poolclass=pool.NullPool,
35
+ )
36
+ with connectable.connect() as connection:
37
+ context.configure(connection=connection, target_metadata=target_metadata)
38
+ with context.begin_transaction():
39
+ context.run_migrations()
40
+
41
+
42
+ if context.is_offline_mode():
43
+ run_migrations_offline()
44
+ else:
45
+ run_migrations_online()
@@ -0,0 +1,26 @@
1
+ """${message}
2
+
3
+ Revision ID: ${up_revision}
4
+ Revises: ${down_revision | comma,n}
5
+ Create Date: ${create_date}
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+ import sqlmodel
13
+ ${imports if imports else ""}
14
+
15
+ revision: str = ${repr(up_revision)}
16
+ down_revision: Union[str, None] = ${repr(down_revision)}
17
+ branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
18
+ depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
19
+
20
+
21
+ def upgrade() -> None:
22
+ ${upgrades if upgrades else "pass"}
23
+
24
+
25
+ def downgrade() -> None:
26
+ ${downgrades if downgrades else "pass"}
@@ -0,0 +1,38 @@
1
+ [alembic]
2
+ script_location = src/gtm_admin/migrations
3
+ prepend_sys_path = src
4
+ version_path_separator = os
5
+
6
+ [loggers]
7
+ keys = root,sqlalchemy,alembic
8
+
9
+ [handlers]
10
+ keys = console
11
+
12
+ [formatters]
13
+ keys = generic
14
+
15
+ [logger_root]
16
+ level = WARN
17
+ handlers = console
18
+ qualname =
19
+
20
+ [logger_sqlalchemy]
21
+ level = WARN
22
+ handlers =
23
+ qualname = sqlalchemy.engine
24
+
25
+ [logger_alembic]
26
+ level = INFO
27
+ handlers =
28
+ qualname = alembic
29
+
30
+ [handler_console]
31
+ class = StreamHandler
32
+ args = (sys.stderr,)
33
+ level = NOTSET
34
+ formatter = generic
35
+
36
+ [formatter_generic]
37
+ format = %(levelname)-5.5s [%(name)s] %(message)s
38
+ datefmt = %H:%M:%S
File without changes
@@ -0,0 +1,21 @@
1
+ from datetime import datetime, timedelta, timezone
2
+
3
+ from jose import jwt
4
+ from passlib.context import CryptContext
5
+
6
+ from app.config import settings
7
+
8
+ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
9
+
10
+
11
+ def hash_password(password: str) -> str:
12
+ return pwd_context.hash(password)
13
+
14
+
15
+ def verify_password(plain: str, hashed: str) -> bool:
16
+ return pwd_context.verify(plain, hashed)
17
+
18
+
19
+ def create_access_token(subject: str) -> str:
20
+ expire = datetime.now(timezone.utc) + timedelta(minutes=settings.access_token_expire_minutes)
21
+ return jwt.encode({"sub": subject, "exp": expire}, settings.secret_key, algorithm="HS256")
@@ -0,0 +1,16 @@
1
+ from pydantic_settings import BaseSettings
2
+
3
+
4
+ class Settings(BaseSettings):
5
+ database_url: str
6
+ secret_key: str
7
+ access_token_expire_minutes: int = 30
8
+
9
+ @property
10
+ def async_database_url(self) -> str:
11
+ return self.database_url.replace("postgresql://", "postgresql+asyncpg://", 1)
12
+
13
+ model_config = {"env_file": ".env"}
14
+
15
+
16
+ settings = Settings()
@@ -0,0 +1,16 @@
1
+ from collections.abc import AsyncGenerator
2
+
3
+ from sqlalchemy.ext.asyncio import create_async_engine
4
+ from sqlalchemy.orm import sessionmaker
5
+ from sqlmodel.ext.asyncio.session import AsyncSession
6
+
7
+ from app.config import settings
8
+
9
+ engine = create_async_engine(settings.async_database_url)
10
+
11
+ AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
12
+
13
+
14
+ async def get_session() -> AsyncGenerator[AsyncSession, None]:
15
+ async with AsyncSessionLocal() as session:
16
+ yield session
@@ -0,0 +1,33 @@
1
+ from fastapi import Depends, HTTPException, status
2
+ from fastapi.security import OAuth2PasswordBearer
3
+ from jose import JWTError, jwt
4
+ from sqlmodel.ext.asyncio.session import AsyncSession
5
+
6
+ from app.config import settings
7
+ from app.database import get_session
8
+ from app.models.user import User
9
+
10
+ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token")
11
+
12
+
13
+ async def get_current_user(
14
+ token: str = Depends(oauth2_scheme),
15
+ session: AsyncSession = Depends(get_session),
16
+ ) -> User:
17
+ credentials_exception = HTTPException(
18
+ status_code=status.HTTP_401_UNAUTHORIZED,
19
+ detail="Invalid credentials",
20
+ headers={"WWW-Authenticate": "Bearer"},
21
+ )
22
+ try:
23
+ payload = jwt.decode(token, settings.secret_key, algorithms=["HS256"])
24
+ username: str | None = payload.get("sub")
25
+ if username is None:
26
+ raise credentials_exception
27
+ except JWTError:
28
+ raise credentials_exception
29
+
30
+ user = await session.get(User, username)
31
+ if user is None:
32
+ raise credentials_exception
33
+ return user
@@ -0,0 +1,8 @@
1
+ from fastapi import FastAPI
2
+
3
+ from app.routers import auth, workflow_runs
4
+
5
+ app = FastAPI(title="GTM Admin API")
6
+
7
+ app.include_router(auth.router)
8
+ app.include_router(workflow_runs.router)
@@ -0,0 +1,4 @@
1
+ from app.models.user import User
2
+ from app.models.workflow_run import WorkflowRun
3
+
4
+ __all__ = ["User", "WorkflowRun"]
@@ -0,0 +1,22 @@
1
+ from datetime import datetime, timezone
2
+
3
+ from sqlmodel import Field, SQLModel
4
+
5
+
6
+ class UserBase(SQLModel):
7
+ username: str = Field(primary_key=True)
8
+
9
+
10
+ class User(UserBase, table=True):
11
+ __tablename__ = "users"
12
+
13
+ hashed_password: str
14
+ created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
15
+
16
+
17
+ class UserCreate(UserBase):
18
+ password: str
19
+
20
+
21
+ class UserRead(UserBase):
22
+ created_at: datetime
@@ -0,0 +1,42 @@
1
+ from datetime import datetime, timezone
2
+ from enum import Enum
3
+ from uuid import UUID, uuid4
4
+
5
+ from sqlmodel import Field, SQLModel
6
+
7
+
8
+ class WorkflowStatus(str, Enum):
9
+ pending = "pending"
10
+ running = "running"
11
+ success = "success"
12
+ failed = "failed"
13
+
14
+
15
+ class WorkflowRunBase(SQLModel):
16
+ name: str
17
+ status: WorkflowStatus = WorkflowStatus.pending
18
+ started_at: datetime | None = None
19
+ finished_at: datetime | None = None
20
+
21
+
22
+ class WorkflowRun(WorkflowRunBase, table=True):
23
+ __tablename__ = "workflow_runs"
24
+
25
+ id: UUID = Field(default_factory=uuid4, primary_key=True)
26
+ created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
27
+
28
+
29
+ class WorkflowRunCreate(WorkflowRunBase):
30
+ pass
31
+
32
+
33
+ class WorkflowRunUpdate(SQLModel):
34
+ name: str | None = None
35
+ status: WorkflowStatus | None = None
36
+ started_at: datetime | None = None
37
+ finished_at: datetime | None = None
38
+
39
+
40
+ class WorkflowRunRead(WorkflowRunBase):
41
+ id: UUID
42
+ created_at: datetime
File without changes
@@ -0,0 +1,31 @@
1
+ from fastapi import APIRouter, Depends, HTTPException, status
2
+ from fastapi.security import OAuth2PasswordRequestForm
3
+ from sqlmodel.ext.asyncio.session import AsyncSession
4
+
5
+ from app.auth import create_access_token, hash_password, verify_password
6
+ from app.database import get_session
7
+ from app.models.user import User, UserCreate, UserRead
8
+
9
+ router = APIRouter(prefix="/auth", tags=["auth"])
10
+
11
+
12
+ @router.post("/token")
13
+ async def login(
14
+ form: OAuth2PasswordRequestForm = Depends(),
15
+ session: AsyncSession = Depends(get_session),
16
+ ):
17
+ user = await session.get(User, form.username)
18
+ if not user or not verify_password(form.password, user.hashed_password):
19
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
20
+ return {"access_token": create_access_token(user.username), "token_type": "bearer"}
21
+
22
+
23
+ @router.post("/register", response_model=UserRead, status_code=status.HTTP_201_CREATED)
24
+ async def register(body: UserCreate, session: AsyncSession = Depends(get_session)):
25
+ if await session.get(User, body.username):
26
+ raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Username already exists")
27
+ user = User(username=body.username, hashed_password=hash_password(body.password))
28
+ session.add(user)
29
+ await session.commit()
30
+ await session.refresh(user)
31
+ return user
@@ -0,0 +1,77 @@
1
+ from uuid import UUID
2
+
3
+ from fastapi import APIRouter, Depends, HTTPException, status
4
+ from sqlmodel import select
5
+ from sqlmodel.ext.asyncio.session import AsyncSession
6
+
7
+ from app.database import get_session
8
+ from app.deps import get_current_user
9
+ from app.models.user import User
10
+ from app.models.workflow_run import WorkflowRun, WorkflowRunCreate, WorkflowRunRead, WorkflowRunUpdate
11
+
12
+ router = APIRouter(prefix="/workflow-runs", tags=["workflow-runs"])
13
+
14
+
15
+ @router.get("", response_model=list[WorkflowRunRead])
16
+ async def list_workflow_runs(
17
+ session: AsyncSession = Depends(get_session),
18
+ _: User = Depends(get_current_user),
19
+ ):
20
+ result = await session.exec(select(WorkflowRun))
21
+ return result.all()
22
+
23
+
24
+ @router.get("/{id}", response_model=WorkflowRunRead)
25
+ async def get_workflow_run(
26
+ id: UUID,
27
+ session: AsyncSession = Depends(get_session),
28
+ _: User = Depends(get_current_user),
29
+ ):
30
+ run = await session.get(WorkflowRun, id)
31
+ if not run:
32
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Workflow run not found")
33
+ return run
34
+
35
+
36
+ @router.post("", response_model=WorkflowRunRead, status_code=status.HTTP_201_CREATED)
37
+ async def create_workflow_run(
38
+ body: WorkflowRunCreate,
39
+ session: AsyncSession = Depends(get_session),
40
+ _: User = Depends(get_current_user),
41
+ ):
42
+ run = WorkflowRun.model_validate(body)
43
+ session.add(run)
44
+ await session.commit()
45
+ await session.refresh(run)
46
+ return run
47
+
48
+
49
+ @router.patch("/{id}", response_model=WorkflowRunRead)
50
+ async def update_workflow_run(
51
+ id: UUID,
52
+ body: WorkflowRunUpdate,
53
+ session: AsyncSession = Depends(get_session),
54
+ _: User = Depends(get_current_user),
55
+ ):
56
+ run = await session.get(WorkflowRun, id)
57
+ if not run:
58
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Workflow run not found")
59
+ for field, value in body.model_dump(exclude_unset=True).items():
60
+ setattr(run, field, value)
61
+ session.add(run)
62
+ await session.commit()
63
+ await session.refresh(run)
64
+ return run
65
+
66
+
67
+ @router.delete("/{id}", status_code=status.HTTP_204_NO_CONTENT)
68
+ async def delete_workflow_run(
69
+ id: UUID,
70
+ session: AsyncSession = Depends(get_session),
71
+ _: User = Depends(get_current_user),
72
+ ):
73
+ run = await session.get(WorkflowRun, id)
74
+ if not run:
75
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Workflow run not found")
76
+ await session.delete(run)
77
+ await session.commit()