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.
- gtm_admin-0.1.0/.env.example +3 -0
- gtm_admin-0.1.0/.gitignore +43 -0
- gtm_admin-0.1.0/LICENSE +21 -0
- gtm_admin-0.1.0/Makefile +10 -0
- gtm_admin-0.1.0/PKG-INFO +130 -0
- gtm_admin-0.1.0/README.md +80 -0
- gtm_admin-0.1.0/alembic/env.py +45 -0
- gtm_admin-0.1.0/alembic/script.py.mako +26 -0
- gtm_admin-0.1.0/alembic.ini +38 -0
- gtm_admin-0.1.0/app/__init__.py +0 -0
- gtm_admin-0.1.0/app/auth.py +21 -0
- gtm_admin-0.1.0/app/config.py +16 -0
- gtm_admin-0.1.0/app/database.py +16 -0
- gtm_admin-0.1.0/app/deps.py +33 -0
- gtm_admin-0.1.0/app/main.py +8 -0
- gtm_admin-0.1.0/app/models/__init__.py +4 -0
- gtm_admin-0.1.0/app/models/user.py +22 -0
- gtm_admin-0.1.0/app/models/workflow_run.py +42 -0
- gtm_admin-0.1.0/app/routers/__init__.py +0 -0
- gtm_admin-0.1.0/app/routers/auth.py +31 -0
- gtm_admin-0.1.0/app/routers/workflow_runs.py +77 -0
- gtm_admin-0.1.0/hatch_build.py +31 -0
- gtm_admin-0.1.0/pyproject.toml +46 -0
- gtm_admin-0.1.0/src/gtm_admin/__init__.py +3 -0
- gtm_admin-0.1.0/src/gtm_admin/auth.py +34 -0
- gtm_admin-0.1.0/src/gtm_admin/core.py +66 -0
- gtm_admin-0.1.0/src/gtm_admin/database.py +46 -0
- gtm_admin-0.1.0/src/gtm_admin/decorator.py +58 -0
- gtm_admin-0.1.0/src/gtm_admin/deps.py +27 -0
- gtm_admin-0.1.0/src/gtm_admin/migrations/env.py +52 -0
- gtm_admin-0.1.0/src/gtm_admin/migrations/script.py.mako +26 -0
- gtm_admin-0.1.0/src/gtm_admin/migrations/versions/.gitkeep +0 -0
- gtm_admin-0.1.0/src/gtm_admin/models.py +27 -0
- gtm_admin-0.1.0/src/gtm_admin/router.py +77 -0
- gtm_admin-0.1.0/src/gtm_admin/static/assets/index-BTDQOZNI.js +485 -0
- gtm_admin-0.1.0/src/gtm_admin/static/assets/index-fyDHVEth.css +1 -0
- gtm_admin-0.1.0/src/gtm_admin/static/index.html +14 -0
- gtm_admin-0.1.0/uv.lock +1680 -0
|
@@ -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
|
gtm_admin-0.1.0/LICENSE
ADDED
|
@@ -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.
|
gtm_admin-0.1.0/Makefile
ADDED
gtm_admin-0.1.0/PKG-INFO
ADDED
|
@@ -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,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()
|