ducta 0.1.0__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.
- ducta/__init__.py +39 -0
- ducta/api/README.md +146 -0
- ducta/api/__init__.py +267 -0
- ducta/api/auth/__init__.py +26 -0
- ducta/api/auth/service.py +154 -0
- ducta/api/auth/users.py +144 -0
- ducta/api/config.py +304 -0
- ducta/api/core/__init__.py +29 -0
- ducta/api/core/file_watcher.py +112 -0
- ducta/api/core/git_sync.py +325 -0
- ducta/api/core/locks.py +156 -0
- ducta/api/db/__init__.py +24 -0
- ducta/api/db/base.py +29 -0
- ducta/api/db/engine.py +83 -0
- ducta/api/db/migrate.py +84 -0
- ducta/api/db/migrations/__init__.py +21 -0
- ducta/api/db/migrations/env.py +137 -0
- ducta/api/db/migrations/versions/0001_initial.py +140 -0
- ducta/api/db/migrations/versions/0002_add_execution_user_id.py +49 -0
- ducta/api/db/migrations/versions/0003_add_execution_sweep_fields.py +50 -0
- ducta/api/db/migrations/versions/0004_users_email_not_null.py +64 -0
- ducta/api/db/models.py +142 -0
- ducta/api/db/session.py +64 -0
- ducta/api/db/stores/__init__.py +21 -0
- ducta/api/db/stores/execution_store.py +282 -0
- ducta/api/db/stores/user_store.py +197 -0
- ducta/api/dependencies.py +260 -0
- ducta/api/exceptions.py +158 -0
- ducta/api/execution/__init__.py +25 -0
- ducta/api/execution/buffering.py +246 -0
- ducta/api/execution/context.py +37 -0
- ducta/api/execution/error_recovery.py +370 -0
- ducta/api/execution/file_log_store.py +173 -0
- ducta/api/execution/isolation.py +146 -0
- ducta/api/execution/log_sink.py +94 -0
- ducta/api/execution/manager.py +832 -0
- ducta/api/execution/output_capture.py +191 -0
- ducta/api/execution/queue.py +271 -0
- ducta/api/execution/resilience_core.py +162 -0
- ducta/api/execution/resilience_helpers.py +76 -0
- ducta/api/execution/runner.py +615 -0
- ducta/api/execution/scheduler.py +249 -0
- ducta/api/execution/store.py +157 -0
- ducta/api/execution/timeout.py +192 -0
- ducta/api/main.py +252 -0
- ducta/api/middleware/__init__.py +181 -0
- ducta/api/middleware/audit.py +94 -0
- ducta/api/middleware/rate_limit.py +397 -0
- ducta/api/models/__init__.py +68 -0
- ducta/api/models/auth.py +125 -0
- ducta/api/models/config.py +48 -0
- ducta/api/models/execution.py +292 -0
- ducta/api/models/git.py +121 -0
- ducta/api/models/git_sync.py +114 -0
- ducta/api/models/ingestion.py +97 -0
- ducta/api/models/node_schema.py +76 -0
- ducta/api/models/project.py +133 -0
- ducta/api/models/quality.py +82 -0
- ducta/api/models/repository.py +91 -0
- ducta/api/models/responses.py +108 -0
- ducta/api/models/template.py +51 -0
- ducta/api/models/workspace.py +98 -0
- ducta/api/repositories/__init__.py +33 -0
- ducta/api/repositories/config_repository.py +104 -0
- ducta/api/repositories/node_repository.py +398 -0
- ducta/api/repositories/pipeline_repository.py +121 -0
- ducta/api/repositories/project_repository.py +326 -0
- ducta/api/repository/__init__.py +33 -0
- ducta/api/repository/aws.py +205 -0
- ducta/api/repository/azure.py +177 -0
- ducta/api/repository/base.py +83 -0
- ducta/api/repository/github.py +163 -0
- ducta/api/repository/local.py +107 -0
- ducta/api/routes/__init__.py +109 -0
- ducta/api/routes/auth.py +194 -0
- ducta/api/routes/certificates.py +154 -0
- ducta/api/routes/configs.py +129 -0
- ducta/api/routes/connect.py +95 -0
- ducta/api/routes/environments.py +51 -0
- ducta/api/routes/execution.py +829 -0
- ducta/api/routes/git.py +452 -0
- ducta/api/routes/health.py +59 -0
- ducta/api/routes/ingestion.py +238 -0
- ducta/api/routes/mlops.py +359 -0
- ducta/api/routes/nodes.py +384 -0
- ducta/api/routes/projects.py +590 -0
- ducta/api/routes/quality.py +257 -0
- ducta/api/routes/repository.py +316 -0
- ducta/api/routes/schedules.py +118 -0
- ducta/api/routes/templates.py +100 -0
- ducta/api/routes/terminal.py +209 -0
- ducta/api/routes/workspace.py +249 -0
- ducta/api/routes/workspace_files.py +91 -0
- ducta/api/services/__init__.py +37 -0
- ducta/api/services/config_cache.py +125 -0
- ducta/api/services/config_service.py +109 -0
- ducta/api/services/node_schema_service.py +266 -0
- ducta/api/services/node_service.py +194 -0
- ducta/api/services/project.py +429 -0
- ducta/api/source/__init__.py +24 -0
- ducta/api/source/models.py +59 -0
- ducta/api/source/resolver.py +419 -0
- ducta/api/utils/__init__.py +19 -0
- ducta/api/utils/audit.py +76 -0
- ducta/api/utils/git_utils.py +197 -0
- ducta/api/utils/logging.py +55 -0
- ducta/api/utils/platform_utils.py +83 -0
- ducta/api/utils/validators.py +132 -0
- ducta/api/workspace/__init__.py +23 -0
- ducta/api/workspace/loaders.py +267 -0
- ducta/api/workspace/manager.py +372 -0
- ducta/api/workspace/preflight.py +244 -0
- ducta/api/workspace/structure_detector.py +223 -0
- ducta/api/workspace/utils.py +122 -0
- ducta/check/README.md +168 -0
- ducta/check/__init__.py +186 -0
- ducta/check/advisor.py +110 -0
- ducta/check/checks/__init__.py +66 -0
- ducta/check/checks/business.py +312 -0
- ducta/check/checks/cross_table.py +214 -0
- ducta/check/checks/distribution.py +395 -0
- ducta/check/checks/structural.py +507 -0
- ducta/check/checks/temporal.py +383 -0
- ducta/check/core.py +509 -0
- ducta/check/engine.py +1130 -0
- ducta/check/gate.py +213 -0
- ducta/check/output_manager.py +237 -0
- ducta/check/profiles.py +126 -0
- ducta/check/serialization.py +125 -0
- ducta/check/service.py +379 -0
- ducta/check/storage.py +627 -0
- ducta/console/README.md +150 -0
- ducta/console/__init__.py +214 -0
- ducta/console/cli.py +388 -0
- ducta/console/commands/__init__.py +26 -0
- ducta/console/commands/certify_cmds.py +213 -0
- ducta/console/commands/config_cmds.py +315 -0
- ducta/console/commands/execution_cmds.py +503 -0
- ducta/console/commands/ingestion_cmds.py +45 -0
- ducta/console/commands/ingestion_setup.py +284 -0
- ducta/console/commands/quality_cmds.py +356 -0
- ducta/console/config.py +449 -0
- ducta/console/core.py +420 -0
- ducta/console/execution.py +288 -0
- ducta/console/mlops_commands.py +175 -0
- ducta/console/parser.py +658 -0
- ducta/console/template.py +1683 -0
- ducta/console/ui.py +169 -0
- ducta/console/ux/__init__.py +78 -0
- ducta/console/ux/error_analyzer.py +387 -0
- ducta/console/ux/formatters.py +278 -0
- ducta/console/ux/rich_logger.py +326 -0
- ducta/console/ux/schema_formatter.py +194 -0
- ducta/console/validation.py +447 -0
- ducta/console/wrapper.py +108 -0
- ducta/core/README.md +165 -0
- ducta/core/__init__.py +228 -0
- ducta/core/certificate.py +388 -0
- ducta/core/commands.py +377 -0
- ducta/core/dependency_inference.py +177 -0
- ducta/core/dependency_resolver.py +136 -0
- ducta/core/execution_context.py +42 -0
- ducta/core/executor.py +2029 -0
- ducta/core/import_security.py +280 -0
- ducta/core/ml_context.py +81 -0
- ducta/core/ml_node_validator.py +197 -0
- ducta/core/mlflow_node_executor.py +214 -0
- ducta/core/mlops_auto_config.py +177 -0
- ducta/core/mlops_integration.py +610 -0
- ducta/core/node_executor.py +1932 -0
- ducta/core/pipeline_dependency_resolver.py +111 -0
- ducta/core/pipeline_state.py +690 -0
- ducta/core/pipeline_validator.py +625 -0
- ducta/core/preflight.py +311 -0
- ducta/core/resilience.py +65 -0
- ducta/core/resource_manager.py +436 -0
- ducta/core/split_validator.py +195 -0
- ducta/core/storage/__init__.py +35 -0
- ducta/core/storage/base.py +220 -0
- ducta/core/sweep.py +66 -0
- ducta/core/utils.py +173 -0
- ducta/gate/README.md +235 -0
- ducta/gate/__init__.py +128 -0
- ducta/gate/base.py +139 -0
- ducta/gate/concurrency.py +74 -0
- ducta/gate/constants.py +68 -0
- ducta/gate/context_manager.py +121 -0
- ducta/gate/exceptions.py +64 -0
- ducta/gate/factories.py +128 -0
- ducta/gate/fingerprinting.py +71 -0
- ducta/gate/gateway/__init__.py +28 -0
- ducta/gate/gateway/circuit_breaker.py +101 -0
- ducta/gate/gateway/connector.py +399 -0
- ducta/gate/gateway/exceptions.py +28 -0
- ducta/gate/gateway/manager.py +453 -0
- ducta/gate/gateway/service.py +220 -0
- ducta/gate/gateway/spark_setup.py +91 -0
- ducta/gate/handoff.py +144 -0
- ducta/gate/input.py +360 -0
- ducta/gate/output/__init__.py +39 -0
- ducta/gate/output/dataframes.py +112 -0
- ducta/gate/output/manager.py +353 -0
- ducta/gate/output/paths.py +135 -0
- ducta/gate/output/unity_catalog.py +300 -0
- ducta/gate/readers.py +313 -0
- ducta/gate/sql.py +492 -0
- ducta/gate/validators.py +158 -0
- ducta/gate/writers.py +275 -0
- ducta/mlrun/README.md +159 -0
- ducta/mlrun/__init__.py +310 -0
- ducta/mlrun/base.py +532 -0
- ducta/mlrun/baseline.py +126 -0
- ducta/mlrun/cache.py +743 -0
- ducta/mlrun/concurrency.py +971 -0
- ducta/mlrun/config.py +703 -0
- ducta/mlrun/environment.py +156 -0
- ducta/mlrun/error_analysis.py +214 -0
- ducta/mlrun/events.py +1111 -0
- ducta/mlrun/exceptions.py +786 -0
- ducta/mlrun/experiment_tracking.py +1466 -0
- ducta/mlrun/fingerprint.py +186 -0
- ducta/mlrun/gc.py +110 -0
- ducta/mlrun/health.py +703 -0
- ducta/mlrun/hyperparams.py +644 -0
- ducta/mlrun/mlflow.py +1001 -0
- ducta/mlrun/model_registry.py +869 -0
- ducta/mlrun/persistence.py +132 -0
- ducta/mlrun/protocols.py +530 -0
- ducta/mlrun/resilience.py +626 -0
- ducta/mlrun/split.py +204 -0
- ducta/mlrun/storage.py +1253 -0
- ducta/mlrun/validators.py +579 -0
- ducta/setting/README.md +194 -0
- ducta/setting/__init__.py +175 -0
- ducta/setting/config_forms.py +212 -0
- ducta/setting/context_loader.py +109 -0
- ducta/setting/contexts.py +919 -0
- ducta/setting/environments.py +191 -0
- ducta/setting/exceptions.py +55 -0
- ducta/setting/interpolator.py +195 -0
- ducta/setting/layered_config.py +483 -0
- ducta/setting/loaders.py +270 -0
- ducta/setting/schemas.py +816 -0
- ducta/setting/session.py +528 -0
- ducta/setting/utils.py +34 -0
- ducta/setting/validators.py +522 -0
- ducta/stream/README.md +145 -0
- ducta/stream/__init__.py +115 -0
- ducta/stream/checkpoints.py +208 -0
- ducta/stream/constants.py +130 -0
- ducta/stream/context_utils.py +33 -0
- ducta/stream/exceptions.py +262 -0
- ducta/stream/pipeline_manager.py +1328 -0
- ducta/stream/progress_listener.py +175 -0
- ducta/stream/query_manager.py +942 -0
- ducta/stream/readers.py +495 -0
- ducta/stream/trigger_scheduler.py +247 -0
- ducta/stream/validators.py +1131 -0
- ducta/stream/writers.py +369 -0
- ducta/ui/dist/assets/ActionButton-DMlTJENg.js +1 -0
- ducta/ui/dist/assets/CodeEditor-Bv79wUIa.js +17 -0
- ducta/ui/dist/assets/CodeEditorModal-Bn6HIvo_.js +2 -0
- ducta/ui/dist/assets/FileTree-fy0K1hc5.js +11 -0
- ducta/ui/dist/assets/GitPage-BLZTifzs.js +1 -0
- ducta/ui/dist/assets/IconDeviceFloppy-DJx3woB0.js +6 -0
- ducta/ui/dist/assets/IconFile-BOwCvB1N.js +6 -0
- ducta/ui/dist/assets/IconGauge-C7_OWA3r.js +6 -0
- ducta/ui/dist/assets/IconRefresh--9MuA2Po.js +6 -0
- ducta/ui/dist/assets/IngestionPage-BejE_vmJ.js +16 -0
- ducta/ui/dist/assets/NodeCodePage-BDebkLnk.js +2 -0
- ducta/ui/dist/assets/PageHeader-CirL0F2V.js +1 -0
- ducta/ui/dist/assets/SchedulesPage-OXeqg9bk.js +6 -0
- ducta/ui/dist/assets/SlidePanel-DvSLs86R.js +1 -0
- ducta/ui/dist/assets/TerminalPage-CRnPkCpN.js +6 -0
- ducta/ui/dist/assets/index-5f-7Noni.js +6 -0
- ducta/ui/dist/assets/index-C3Z4BcMk.js +16 -0
- ducta/ui/dist/assets/index-D5LWWk6j.js +410 -0
- ducta/ui/dist/assets/index-DLMDRgWR.css +1 -0
- ducta/ui/dist/assets/index-DtQrMoBQ.js +6 -0
- ducta/ui/dist/assets/logs.worker-OYhZsjGZ.js +1 -0
- ducta/ui/dist/assets/vendor-query-Dhnd9gHa.js +1 -0
- ducta/ui/dist/assets/vendor-react-DX4KxU8z.js +50 -0
- ducta/ui/dist/assets/vendor-router-Bmx9pU3G.js +13 -0
- ducta/ui/dist/assets/vendor-state-y7_o29KJ.js +1 -0
- ducta/ui/dist/assets/vendor-utils-BxtLg8Md.js +37 -0
- ducta/ui/dist/index.html +22 -0
- ducta/ui/scripts/contrast-check.mjs +150 -0
- ducta/ui/scripts/ui-quality-check.mjs +62 -0
- ducta-0.1.0.dist-info/METADATA +358 -0
- ducta-0.1.0.dist-info/RECORD +293 -0
- ducta-0.1.0.dist-info/WHEEL +4 -0
- ducta-0.1.0.dist-info/entry_points.txt +3 -0
- ducta-0.1.0.dist-info/licenses/LICENSE +201 -0
ducta/__init__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (C) 2024-2026 Faustino Lopez Ramos
|
|
3
|
+
|
|
4
|
+
This file is part of ducta.
|
|
5
|
+
|
|
6
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
7
|
+
use this file except in compliance with the License. You may obtain a copy
|
|
8
|
+
of the License at
|
|
9
|
+
|
|
10
|
+
https://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
|
|
12
|
+
Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
14
|
+
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
15
|
+
License for the specific language governing permissions and limitations
|
|
16
|
+
under the License.
|
|
17
|
+
|
|
18
|
+
SPDX-License-Identifier: Apache-2.0
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
24
|
+
|
|
25
|
+
from loguru import logger
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
__version__ = version("ducta")
|
|
29
|
+
except PackageNotFoundError: # pragma: no cover - not installed (e.g. running from source)
|
|
30
|
+
__version__ = "0.0.0+unknown"
|
|
31
|
+
|
|
32
|
+
# Silence the library by default (standard loguru pattern for libraries).
|
|
33
|
+
# Ducta emits no log records unless the consuming application explicitly turns
|
|
34
|
+
# them on with ``logger.enable("ducta")``. The CLI and API server do this when
|
|
35
|
+
# they configure their own handlers. This guarantees Ducta's internal module
|
|
36
|
+
# paths (its core) are never exposed to end users through stray import-time logs.
|
|
37
|
+
logger.disable("ducta")
|
|
38
|
+
|
|
39
|
+
__all__ = ["__version__"]
|
ducta/api/README.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Ducta API
|
|
2
|
+
|
|
3
|
+
> **The REST + WebSocket server of Ducta.** Built on **FastAPI**, it exposes pipeline configuration, execution, git history, data quality, MLOps, and workspace management over HTTP — and serves the bundled web UI. Sources (local paths or Git URLs) are provided per request, so one server can drive many projects.
|
|
4
|
+
|
|
5
|
+
## At a glance
|
|
6
|
+
|
|
7
|
+
| | |
|
|
8
|
+
|---|---|
|
|
9
|
+
| **Purpose** | Expose Ducta over HTTP/WS for tools, automation, and the web UI |
|
|
10
|
+
| **Layer** | Interface (service) |
|
|
11
|
+
| **Depends on** | `core`, `setting`, `console`; optional SQLAlchemy persistence |
|
|
12
|
+
| **Used by** | `ui` and any external HTTP client |
|
|
13
|
+
| **Key entry points** | `create_app()` / `ducta.api.main:app`, routers under `/api`, `ducta server start` |
|
|
14
|
+
| **Install** | `pip install "ducta[api]"` |
|
|
15
|
+
|
|
16
|
+
## Where it fits
|
|
17
|
+
|
|
18
|
+
```mermaid
|
|
19
|
+
flowchart TD
|
|
20
|
+
console["console · CLI"] --> core
|
|
21
|
+
api["api · REST / WebSocket"] --> core
|
|
22
|
+
ui["ui · Web UI"] --> api
|
|
23
|
+
setting["setting · Config foundation"] --> core["core · Orchestration"]
|
|
24
|
+
core --> gate["gate · Data I/O"]
|
|
25
|
+
core --> stream["stream · Streaming"]
|
|
26
|
+
core --> check["check · Data quality"]
|
|
27
|
+
core --> mlrun["mlrun · MLOps"]
|
|
28
|
+
style api fill:#4f46e5,stroke:#312e81,color:#fff
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 1. Overview
|
|
34
|
+
|
|
35
|
+
### For Non-Technical Users
|
|
36
|
+
Ducta API is the **backend brain** behind the web app and any automation. It lets tools and teammates — not just one person at a terminal — start pipelines, watch them run live, browse configuration, review data-quality results, and manage models, all through a standard web interface with optional login.
|
|
37
|
+
|
|
38
|
+
### For Technical Users
|
|
39
|
+
Ducta API is the service layer, implementing:
|
|
40
|
+
* **App factory & lifespan**: `create_app()` wires middleware, routes, exception handlers, optional DB persistence + migrations, the execution manager, and a config cache; it warns when bound to a non-local address without auth (`main.py`).
|
|
41
|
+
* **Routers under `/api`**: auth, execution (+ WebSocket log streaming), configs, environments, git, repository, projects, nodes, mlops, quality, ingestion, templates, workspace, certificates, connect, plus a `/ws` terminal and unauthenticated health probes (`routes/`).
|
|
42
|
+
* **Optional JWT auth**: `AuthService` (bcrypt password hashing, jose-signed HS256 tokens with type/expiry checks); production settings refuse the default secret and require `auth_enabled` (`auth/`, `config.py`).
|
|
43
|
+
* **Execution engine**: an in-memory `ExecutionManager` with a bounded queue, concurrent-run limits, log buffering, and optional DB-backed persistence layered transparently (`execution/`).
|
|
44
|
+
* **Source resolution with SSRF defense**: per-request local paths are security-validated; Git clones are host-allow-listed and reject internal/non-routable targets (loopback, private, link-local metadata endpoint) (`source/resolver.py`).
|
|
45
|
+
* **Persistence & repositories**: optional SQLAlchemy-async engine, Alembic-style migrations, and repository/store abstractions for projects, nodes, and execution history (`db/`, `repositories/`, `repository/`).
|
|
46
|
+
* **Middleware**: request-ID tagging, CORS, and configurable per-IP rate limiting (in-memory or Redis) (`middleware/`).
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## 2. Configuration
|
|
51
|
+
|
|
52
|
+
Settings come from environment variables (Pydantic `Settings`, `config.py`). Key variables:
|
|
53
|
+
|
|
54
|
+
| Variable | Default | Purpose |
|
|
55
|
+
|----------|---------|---------|
|
|
56
|
+
| `HOST` / `PORT` | `127.0.0.1` / `8000` | Bind address (non-local without auth triggers a warning) |
|
|
57
|
+
| `ENVIRONMENT` | `development` | `production` enforces auth + a non-default JWT secret |
|
|
58
|
+
| `AUTH_ENABLED` | `false` | Require JWT authentication |
|
|
59
|
+
| `JWT_SECRET_KEY` | `change-me-in-production` | HS256 signing key (must be overridden in prod) |
|
|
60
|
+
| `JWT_EXPIRATION_HOURS` | `24` | Access-token lifetime |
|
|
61
|
+
| `DATABASE_URL` | `""` (in-memory) | `sqlite+aiosqlite:///...` or `postgresql+asyncpg://...` |
|
|
62
|
+
| `RATE_LIMIT_ENABLED` | `false` | Per-IP rate limiting (`RATE_LIMIT_REQUESTS`, `..._WINDOW_SECONDS`, `RATE_LIMIT_REDIS_URL`) |
|
|
63
|
+
| `MAX_CONCURRENT_EXECUTIONS` | `5` | Parallel pipeline runs |
|
|
64
|
+
| `CORS_ORIGINS` | `["*"]` | Allowed origins |
|
|
65
|
+
| `GIT_CLONE_ALLOWED_HOSTS` | `[]` | Allow-list for Git clone hosts (empty = public hosts only, internal blocked) |
|
|
66
|
+
|
|
67
|
+
**Production checklist**: set `ENVIRONMENT=production`, `AUTH_ENABLED=true`, a strong `JWT_SECRET_KEY`, and either bind to `127.0.0.1` or place the server behind an authenticating proxy.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 3. Running the Server
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
# Via the CLI (launches this API + serves the UI)
|
|
75
|
+
ducta server start --host 127.0.0.1 --port 8000
|
|
76
|
+
ducta ui --port 8000 # same server, opens a browser
|
|
77
|
+
|
|
78
|
+
# Directly with uvicorn
|
|
79
|
+
uvicorn ducta.api.main:app --host 127.0.0.1 --port 8000
|
|
80
|
+
|
|
81
|
+
# Production-style env
|
|
82
|
+
export ENVIRONMENT=production
|
|
83
|
+
export AUTH_ENABLED=true
|
|
84
|
+
export JWT_SECRET_KEY="$(openssl rand -hex 32)"
|
|
85
|
+
export DATABASE_URL="postgresql+asyncpg://user:pass@localhost:5432/ducta"
|
|
86
|
+
uvicorn ducta.api.main:app --host 0.0.0.0 --port 8000 --workers 4
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Interactive docs are served at `/docs` (Swagger) and `/redoc`; the OpenAPI schema at `/openapi.json`.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 4. HTTP Quickstart
|
|
94
|
+
|
|
95
|
+
All application endpoints are under the `/api` prefix. Sources are passed per request.
|
|
96
|
+
|
|
97
|
+
### Step 1: Health check (no auth)
|
|
98
|
+
```bash
|
|
99
|
+
curl http://127.0.0.1:8000/health
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Step 2: Authenticate (when AUTH_ENABLED=true)
|
|
103
|
+
```bash
|
|
104
|
+
TOKEN=$(curl -s -X POST http://127.0.0.1:8000/api/auth/login \
|
|
105
|
+
-H "Content-Type: application/json" \
|
|
106
|
+
-d '{"username":"admin","password":"admin"}' | jq -r .access_token)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Step 3: Connect to a source and inspect configs
|
|
110
|
+
```bash
|
|
111
|
+
# Register/resolve a source (local path or Git URL) -> returns a project_id
|
|
112
|
+
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
|
113
|
+
http://127.0.0.1:8000/api/connect -d '{"source":"/path/to/project"}'
|
|
114
|
+
|
|
115
|
+
# List configs for an environment
|
|
116
|
+
curl -H "Authorization: Bearer $TOKEN" \
|
|
117
|
+
"http://127.0.0.1:8000/api/configs/dev?source=/path/to/project"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Step 4: Execute a pipeline
|
|
121
|
+
```bash
|
|
122
|
+
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
|
123
|
+
http://127.0.0.1:8000/api/projects/{project_id}/pipelines/sales_daily/execute \
|
|
124
|
+
-d '{"env":"dev"}'
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Step 5: Manage runs and stream logs
|
|
128
|
+
```bash
|
|
129
|
+
# List / cancel / retry executions
|
|
130
|
+
curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8000/api/executions
|
|
131
|
+
curl -X POST -H "Authorization: Bearer $TOKEN" \
|
|
132
|
+
http://127.0.0.1:8000/api/executions/{execution_id}/cancel
|
|
133
|
+
```
|
|
134
|
+
```
|
|
135
|
+
# Live log stream over WebSocket
|
|
136
|
+
ws://127.0.0.1:8000/api/ws/logs/{execution_id}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Programmatic (ASGI / tests)
|
|
140
|
+
```python
|
|
141
|
+
from fastapi.testclient import TestClient
|
|
142
|
+
from ducta.api.main import app
|
|
143
|
+
|
|
144
|
+
client = TestClient(app)
|
|
145
|
+
assert client.get("/health").status_code == 200
|
|
146
|
+
```
|
ducta/api/__init__.py
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (C) 2024-2026 Faustino Lopez Ramos
|
|
3
|
+
|
|
4
|
+
This file is part of ducta.
|
|
5
|
+
|
|
6
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
7
|
+
use this file except in compliance with the License. You may obtain a copy
|
|
8
|
+
of the License at
|
|
9
|
+
|
|
10
|
+
https://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
|
|
12
|
+
Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
14
|
+
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
15
|
+
License for the specific language governing permissions and limitations
|
|
16
|
+
under the License.
|
|
17
|
+
|
|
18
|
+
SPDX-License-Identifier: Apache-2.0
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from ducta import __version__
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# Auth
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
from ducta.api.auth import AuthService, UserStore, get_user_store
|
|
29
|
+
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
# Configuration
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
from ducta.api.config import Settings, get_settings
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# Core background services
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
from ducta.api.core import ConfigFileWatcher, ConfigLockManager, GitSyncManager
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
# Database
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
from ducta.api.db import get_db_session, get_engine, is_db_enabled
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# Dependencies
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
from ducta.api.dependencies import (
|
|
49
|
+
WebSocketAuthError,
|
|
50
|
+
get_config_service,
|
|
51
|
+
get_current_user,
|
|
52
|
+
get_execution_manager,
|
|
53
|
+
get_git_sync_manager,
|
|
54
|
+
get_node_service,
|
|
55
|
+
get_source_path,
|
|
56
|
+
get_workspace_manager,
|
|
57
|
+
require_permission,
|
|
58
|
+
resolve_websocket_user,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
# Exceptions
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
from ducta.api.exceptions import (
|
|
65
|
+
AuthenticationError,
|
|
66
|
+
ConcurrencyError,
|
|
67
|
+
ConfigFileNotFoundError,
|
|
68
|
+
ConfigValidationError,
|
|
69
|
+
ConflictError,
|
|
70
|
+
DuctaAPIError,
|
|
71
|
+
ExecutionError,
|
|
72
|
+
ExecutionNotFoundError,
|
|
73
|
+
ExpiredTokenError,
|
|
74
|
+
InvalidTokenError,
|
|
75
|
+
NodeNotFoundError,
|
|
76
|
+
NotFoundError,
|
|
77
|
+
PipelineNotFoundError,
|
|
78
|
+
ProjectAlreadyExistsError,
|
|
79
|
+
ProjectNotFoundError,
|
|
80
|
+
RepositoryAdapterError,
|
|
81
|
+
SyntaxValidationError,
|
|
82
|
+
ValidationError,
|
|
83
|
+
WorkspaceNotFoundError,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
# Execution
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
from ducta.api.execution import ExecutionManager
|
|
90
|
+
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
# App factory
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
from ducta.api.main import create_app, warn_if_insecure_exposure
|
|
95
|
+
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
# Middleware
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
from ducta.api.middleware import RateLimitMiddleware, register_middleware
|
|
100
|
+
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
# Models
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
from ducta.api.models import (
|
|
105
|
+
BlameLine,
|
|
106
|
+
CommitInfo,
|
|
107
|
+
ConfigFileResponse,
|
|
108
|
+
ConfigValidationResponse,
|
|
109
|
+
ConnectRequest,
|
|
110
|
+
DiffResponse,
|
|
111
|
+
ExecutionResponse,
|
|
112
|
+
ExecutionStatus,
|
|
113
|
+
GitCommitInfo,
|
|
114
|
+
GitCommitRequest,
|
|
115
|
+
GitCommitResponse,
|
|
116
|
+
GitExternalChangesResponse,
|
|
117
|
+
GitHistoryResponse,
|
|
118
|
+
GitStageRequest,
|
|
119
|
+
GitStageResponse,
|
|
120
|
+
GitStatusResponse,
|
|
121
|
+
LoginRequest,
|
|
122
|
+
PushPullRequest,
|
|
123
|
+
PushPullResponse,
|
|
124
|
+
RepositoryConnectRequest,
|
|
125
|
+
RepositoryInfo,
|
|
126
|
+
StructureInfo,
|
|
127
|
+
TokenResponse,
|
|
128
|
+
User,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# ---------------------------------------------------------------------------
|
|
132
|
+
# Repositories
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
from ducta.api.repositories import (
|
|
135
|
+
ConfigRepository,
|
|
136
|
+
NodeRepository,
|
|
137
|
+
PipelineRepository,
|
|
138
|
+
ProjectRepository,
|
|
139
|
+
)
|
|
140
|
+
from ducta.api.repository import (
|
|
141
|
+
AWSAdapter,
|
|
142
|
+
AzureAdapter,
|
|
143
|
+
GitHubAdapter,
|
|
144
|
+
LocalAdapter,
|
|
145
|
+
RepositoryAdapter,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# ---------------------------------------------------------------------------
|
|
149
|
+
# Routes
|
|
150
|
+
# ---------------------------------------------------------------------------
|
|
151
|
+
from ducta.api.routes import register_routes
|
|
152
|
+
|
|
153
|
+
# ---------------------------------------------------------------------------
|
|
154
|
+
# Services
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
from ducta.api.services import (
|
|
157
|
+
CachedConfigService,
|
|
158
|
+
ConfigService,
|
|
159
|
+
NodeSchemaService,
|
|
160
|
+
NodeService,
|
|
161
|
+
ProjectService,
|
|
162
|
+
get_cached_config_service,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# ---------------------------------------------------------------------------
|
|
166
|
+
# Source resolution
|
|
167
|
+
# ---------------------------------------------------------------------------
|
|
168
|
+
from ducta.api.source import ResolvedSource, SourceInfo, SourceResolver
|
|
169
|
+
|
|
170
|
+
# ---------------------------------------------------------------------------
|
|
171
|
+
# Workspace
|
|
172
|
+
# ---------------------------------------------------------------------------
|
|
173
|
+
from ducta.api.workspace import WorkspaceManager
|
|
174
|
+
|
|
175
|
+
__all__ = [
|
|
176
|
+
"__version__",
|
|
177
|
+
"Settings",
|
|
178
|
+
"get_settings",
|
|
179
|
+
"AuthenticationError",
|
|
180
|
+
"ConfigFileNotFoundError",
|
|
181
|
+
"ConfigValidationError",
|
|
182
|
+
"ConflictError",
|
|
183
|
+
"ConcurrencyError",
|
|
184
|
+
"ExecutionError",
|
|
185
|
+
"ExecutionNotFoundError",
|
|
186
|
+
"ExpiredTokenError",
|
|
187
|
+
"InvalidTokenError",
|
|
188
|
+
"NodeNotFoundError",
|
|
189
|
+
"NotFoundError",
|
|
190
|
+
"PipelineNotFoundError",
|
|
191
|
+
"ProjectAlreadyExistsError",
|
|
192
|
+
"ProjectNotFoundError",
|
|
193
|
+
"RepositoryAdapterError",
|
|
194
|
+
"SyntaxValidationError",
|
|
195
|
+
"DuctaAPIError",
|
|
196
|
+
"ValidationError",
|
|
197
|
+
"WorkspaceNotFoundError",
|
|
198
|
+
"WebSocketAuthError",
|
|
199
|
+
"get_config_service",
|
|
200
|
+
"get_current_user",
|
|
201
|
+
"get_execution_manager",
|
|
202
|
+
"get_git_sync_manager",
|
|
203
|
+
"get_node_service",
|
|
204
|
+
"get_source_path",
|
|
205
|
+
"get_workspace_manager",
|
|
206
|
+
"require_permission",
|
|
207
|
+
"resolve_websocket_user",
|
|
208
|
+
"create_app",
|
|
209
|
+
"warn_if_insecure_exposure",
|
|
210
|
+
"AuthService",
|
|
211
|
+
"UserStore",
|
|
212
|
+
"get_user_store",
|
|
213
|
+
"ConfigFileWatcher",
|
|
214
|
+
"ConfigLockManager",
|
|
215
|
+
"GitSyncManager",
|
|
216
|
+
"get_db_session",
|
|
217
|
+
"get_engine",
|
|
218
|
+
"is_db_enabled",
|
|
219
|
+
"ExecutionManager",
|
|
220
|
+
"get_execution_manager",
|
|
221
|
+
"RateLimitMiddleware",
|
|
222
|
+
"register_middleware",
|
|
223
|
+
"BlameLine",
|
|
224
|
+
"CommitInfo",
|
|
225
|
+
"ConfigFileResponse",
|
|
226
|
+
"ConfigValidationResponse",
|
|
227
|
+
"ConnectRequest",
|
|
228
|
+
"DiffResponse",
|
|
229
|
+
"ExecutionResponse",
|
|
230
|
+
"ExecutionStatus",
|
|
231
|
+
"GitCommitInfo",
|
|
232
|
+
"GitCommitRequest",
|
|
233
|
+
"GitCommitResponse",
|
|
234
|
+
"GitExternalChangesResponse",
|
|
235
|
+
"GitHistoryResponse",
|
|
236
|
+
"GitStageRequest",
|
|
237
|
+
"GitStageResponse",
|
|
238
|
+
"GitStatusResponse",
|
|
239
|
+
"LoginRequest",
|
|
240
|
+
"PushPullRequest",
|
|
241
|
+
"PushPullResponse",
|
|
242
|
+
"RepositoryConnectRequest",
|
|
243
|
+
"RepositoryInfo",
|
|
244
|
+
"StructureInfo",
|
|
245
|
+
"TokenResponse",
|
|
246
|
+
"User",
|
|
247
|
+
"ConfigRepository",
|
|
248
|
+
"NodeRepository",
|
|
249
|
+
"PipelineRepository",
|
|
250
|
+
"ProjectRepository",
|
|
251
|
+
"AWSAdapter",
|
|
252
|
+
"AzureAdapter",
|
|
253
|
+
"GitHubAdapter",
|
|
254
|
+
"LocalAdapter",
|
|
255
|
+
"RepositoryAdapter",
|
|
256
|
+
"register_routes",
|
|
257
|
+
"CachedConfigService",
|
|
258
|
+
"ConfigService",
|
|
259
|
+
"NodeSchemaService",
|
|
260
|
+
"NodeService",
|
|
261
|
+
"ProjectService",
|
|
262
|
+
"get_cached_config_service",
|
|
263
|
+
"ResolvedSource",
|
|
264
|
+
"SourceInfo",
|
|
265
|
+
"SourceResolver",
|
|
266
|
+
"WorkspaceManager",
|
|
267
|
+
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (C) 2024-2026 Faustino Lopez Ramos
|
|
3
|
+
|
|
4
|
+
This file is part of ducta.
|
|
5
|
+
|
|
6
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
7
|
+
use this file except in compliance with the License. You may obtain a copy
|
|
8
|
+
of the License at
|
|
9
|
+
|
|
10
|
+
https://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
|
|
12
|
+
Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
14
|
+
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
15
|
+
License for the specific language governing permissions and limitations
|
|
16
|
+
under the License.
|
|
17
|
+
|
|
18
|
+
SPDX-License-Identifier: Apache-2.0
|
|
19
|
+
|
|
20
|
+
Auth package — JWT service and user management.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from ducta.api.auth.service import AuthService
|
|
24
|
+
from ducta.api.auth.users import UserStore, get_user_store
|
|
25
|
+
|
|
26
|
+
__all__ = ["AuthService", "UserStore", "get_user_store"]
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (C) 2024-2026 Faustino Lopez Ramos
|
|
3
|
+
|
|
4
|
+
This file is part of ducta.
|
|
5
|
+
|
|
6
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
7
|
+
use this file except in compliance with the License. You may obtain a copy
|
|
8
|
+
of the License at
|
|
9
|
+
|
|
10
|
+
https://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
|
|
12
|
+
Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
14
|
+
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
15
|
+
License for the specific language governing permissions and limitations
|
|
16
|
+
under the License.
|
|
17
|
+
|
|
18
|
+
SPDX-License-Identifier: Apache-2.0
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import threading
|
|
24
|
+
from datetime import datetime, timedelta, timezone
|
|
25
|
+
from typing import Optional
|
|
26
|
+
from uuid import uuid4
|
|
27
|
+
|
|
28
|
+
import bcrypt as _bcrypt # type: ignore
|
|
29
|
+
from jose import ExpiredSignatureError, JWTError, jwt # type: ignore
|
|
30
|
+
|
|
31
|
+
from ducta.api.exceptions import ExpiredTokenError, InvalidTokenError
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AuthService:
|
|
35
|
+
"""Handles JWT lifecycle and password operations (basic login only)."""
|
|
36
|
+
|
|
37
|
+
_TOKEN_TYPE_ACCESS = "access"
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
secret_key: str,
|
|
42
|
+
algorithm: str = "HS256",
|
|
43
|
+
access_token_expires_hours: int = 24,
|
|
44
|
+
) -> None:
|
|
45
|
+
self._secret = secret_key
|
|
46
|
+
self._algorithm = algorithm
|
|
47
|
+
self._access_ttl = timedelta(hours=access_token_expires_hours)
|
|
48
|
+
# jti -> expiry. In-memory/per-process (same constraint as the
|
|
49
|
+
# in-memory rate limiter): a logout on one worker won't revoke the
|
|
50
|
+
# token on another. Bounded by each entry's own token expiry, not by
|
|
51
|
+
# size — a revoked jti only needs remembering until it would have
|
|
52
|
+
# expired anyway; _prune_expired_revocations sweeps stale entries.
|
|
53
|
+
self._revoked: dict[str, datetime] = {}
|
|
54
|
+
self._revoked_lock = threading.Lock()
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def _bcrypt_bytes(plain: str) -> bytes:
|
|
58
|
+
"""Encode a password for bcrypt.
|
|
59
|
+
|
|
60
|
+
bcrypt only considers the first 72 bytes and raises on longer inputs in
|
|
61
|
+
recent versions; truncate explicitly so hashing and verification stay
|
|
62
|
+
consistent and never error on long passphrases.
|
|
63
|
+
"""
|
|
64
|
+
return plain.encode("utf-8")[:72]
|
|
65
|
+
|
|
66
|
+
def hash_password(self, plain: str) -> str:
|
|
67
|
+
"""Return a bcrypt hash of *plain*."""
|
|
68
|
+
return _bcrypt.hashpw(self._bcrypt_bytes(plain), _bcrypt.gensalt()).decode("utf-8")
|
|
69
|
+
|
|
70
|
+
def verify_password(self, plain: str, hashed: str) -> bool:
|
|
71
|
+
"""Return True if *plain* matches the bcrypt *hashed* password."""
|
|
72
|
+
return _bcrypt.checkpw(self._bcrypt_bytes(plain), hashed.encode("utf-8"))
|
|
73
|
+
|
|
74
|
+
def create_access_token(self, user_id: str, username: str, roles: list[str]) -> str:
|
|
75
|
+
"""Return a signed JWT access token."""
|
|
76
|
+
now = datetime.now(tz=timezone.utc)
|
|
77
|
+
payload = {
|
|
78
|
+
"sub": user_id,
|
|
79
|
+
"username": username,
|
|
80
|
+
"roles": roles,
|
|
81
|
+
"type": self._TOKEN_TYPE_ACCESS,
|
|
82
|
+
"jti": str(uuid4()),
|
|
83
|
+
"iat": now,
|
|
84
|
+
"exp": now + self._access_ttl,
|
|
85
|
+
}
|
|
86
|
+
return jwt.encode(payload, self._secret, algorithm=self._algorithm)
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def access_token_lifetime_seconds(self) -> int:
|
|
90
|
+
return int(self._access_ttl.total_seconds())
|
|
91
|
+
|
|
92
|
+
def decode_access_token(self, token: str) -> dict:
|
|
93
|
+
"""Decode and validate an access token."""
|
|
94
|
+
try:
|
|
95
|
+
payload = jwt.decode(token, self._secret, algorithms=[self._algorithm])
|
|
96
|
+
except ExpiredSignatureError:
|
|
97
|
+
raise ExpiredTokenError("Token has expired")
|
|
98
|
+
except JWTError:
|
|
99
|
+
raise InvalidTokenError("Token is invalid or has been tampered with")
|
|
100
|
+
|
|
101
|
+
if payload.get("type") != self._TOKEN_TYPE_ACCESS:
|
|
102
|
+
raise InvalidTokenError(
|
|
103
|
+
f"Expected token type '{self._TOKEN_TYPE_ACCESS}', got '{payload.get('type')}'"
|
|
104
|
+
)
|
|
105
|
+
if self.is_revoked(payload.get("jti", "")):
|
|
106
|
+
raise InvalidTokenError("Token has been revoked (logged out)")
|
|
107
|
+
return payload
|
|
108
|
+
|
|
109
|
+
def revoke_token(self, jti: str, expires_at: datetime) -> None:
|
|
110
|
+
"""Revoke a token by its ``jti`` — checked by every subsequent `decode_access_token`.
|
|
111
|
+
|
|
112
|
+
No previously-issued JWT can otherwise be invalidated before its own
|
|
113
|
+
expiry (that's the whole point of a stateless token) — logout only
|
|
114
|
+
cleared the client's cookie, so a captured/leaked token kept working
|
|
115
|
+
for its full lifetime regardless of logout.
|
|
116
|
+
"""
|
|
117
|
+
if not jti:
|
|
118
|
+
return
|
|
119
|
+
with self._revoked_lock:
|
|
120
|
+
self._prune_expired_revocations_locked()
|
|
121
|
+
self._revoked[jti] = expires_at
|
|
122
|
+
|
|
123
|
+
def is_revoked(self, jti: str) -> bool:
|
|
124
|
+
if not jti:
|
|
125
|
+
return False
|
|
126
|
+
with self._revoked_lock:
|
|
127
|
+
return jti in self._revoked
|
|
128
|
+
|
|
129
|
+
def _prune_expired_revocations_locked(self) -> None:
|
|
130
|
+
now = datetime.now(tz=timezone.utc)
|
|
131
|
+
expired = [jti for jti, exp in self._revoked.items() if exp <= now]
|
|
132
|
+
for jti in expired:
|
|
133
|
+
del self._revoked[jti]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
_auth_service: Optional[AuthService] = None
|
|
137
|
+
_auth_service_lock = threading.Lock()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def get_auth_service() -> AuthService:
|
|
141
|
+
"""Return the module-level AuthService singleton (thread-safe)."""
|
|
142
|
+
global _auth_service
|
|
143
|
+
if _auth_service is None:
|
|
144
|
+
with _auth_service_lock:
|
|
145
|
+
if _auth_service is None:
|
|
146
|
+
from ducta.api.config import get_settings
|
|
147
|
+
|
|
148
|
+
s = get_settings()
|
|
149
|
+
_auth_service = AuthService(
|
|
150
|
+
secret_key=s.jwt_secret_key,
|
|
151
|
+
algorithm=s.jwt_algorithm,
|
|
152
|
+
access_token_expires_hours=s.jwt_expiration_hours,
|
|
153
|
+
)
|
|
154
|
+
return _auth_service
|