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.
Files changed (293) hide show
  1. ducta/__init__.py +39 -0
  2. ducta/api/README.md +146 -0
  3. ducta/api/__init__.py +267 -0
  4. ducta/api/auth/__init__.py +26 -0
  5. ducta/api/auth/service.py +154 -0
  6. ducta/api/auth/users.py +144 -0
  7. ducta/api/config.py +304 -0
  8. ducta/api/core/__init__.py +29 -0
  9. ducta/api/core/file_watcher.py +112 -0
  10. ducta/api/core/git_sync.py +325 -0
  11. ducta/api/core/locks.py +156 -0
  12. ducta/api/db/__init__.py +24 -0
  13. ducta/api/db/base.py +29 -0
  14. ducta/api/db/engine.py +83 -0
  15. ducta/api/db/migrate.py +84 -0
  16. ducta/api/db/migrations/__init__.py +21 -0
  17. ducta/api/db/migrations/env.py +137 -0
  18. ducta/api/db/migrations/versions/0001_initial.py +140 -0
  19. ducta/api/db/migrations/versions/0002_add_execution_user_id.py +49 -0
  20. ducta/api/db/migrations/versions/0003_add_execution_sweep_fields.py +50 -0
  21. ducta/api/db/migrations/versions/0004_users_email_not_null.py +64 -0
  22. ducta/api/db/models.py +142 -0
  23. ducta/api/db/session.py +64 -0
  24. ducta/api/db/stores/__init__.py +21 -0
  25. ducta/api/db/stores/execution_store.py +282 -0
  26. ducta/api/db/stores/user_store.py +197 -0
  27. ducta/api/dependencies.py +260 -0
  28. ducta/api/exceptions.py +158 -0
  29. ducta/api/execution/__init__.py +25 -0
  30. ducta/api/execution/buffering.py +246 -0
  31. ducta/api/execution/context.py +37 -0
  32. ducta/api/execution/error_recovery.py +370 -0
  33. ducta/api/execution/file_log_store.py +173 -0
  34. ducta/api/execution/isolation.py +146 -0
  35. ducta/api/execution/log_sink.py +94 -0
  36. ducta/api/execution/manager.py +832 -0
  37. ducta/api/execution/output_capture.py +191 -0
  38. ducta/api/execution/queue.py +271 -0
  39. ducta/api/execution/resilience_core.py +162 -0
  40. ducta/api/execution/resilience_helpers.py +76 -0
  41. ducta/api/execution/runner.py +615 -0
  42. ducta/api/execution/scheduler.py +249 -0
  43. ducta/api/execution/store.py +157 -0
  44. ducta/api/execution/timeout.py +192 -0
  45. ducta/api/main.py +252 -0
  46. ducta/api/middleware/__init__.py +181 -0
  47. ducta/api/middleware/audit.py +94 -0
  48. ducta/api/middleware/rate_limit.py +397 -0
  49. ducta/api/models/__init__.py +68 -0
  50. ducta/api/models/auth.py +125 -0
  51. ducta/api/models/config.py +48 -0
  52. ducta/api/models/execution.py +292 -0
  53. ducta/api/models/git.py +121 -0
  54. ducta/api/models/git_sync.py +114 -0
  55. ducta/api/models/ingestion.py +97 -0
  56. ducta/api/models/node_schema.py +76 -0
  57. ducta/api/models/project.py +133 -0
  58. ducta/api/models/quality.py +82 -0
  59. ducta/api/models/repository.py +91 -0
  60. ducta/api/models/responses.py +108 -0
  61. ducta/api/models/template.py +51 -0
  62. ducta/api/models/workspace.py +98 -0
  63. ducta/api/repositories/__init__.py +33 -0
  64. ducta/api/repositories/config_repository.py +104 -0
  65. ducta/api/repositories/node_repository.py +398 -0
  66. ducta/api/repositories/pipeline_repository.py +121 -0
  67. ducta/api/repositories/project_repository.py +326 -0
  68. ducta/api/repository/__init__.py +33 -0
  69. ducta/api/repository/aws.py +205 -0
  70. ducta/api/repository/azure.py +177 -0
  71. ducta/api/repository/base.py +83 -0
  72. ducta/api/repository/github.py +163 -0
  73. ducta/api/repository/local.py +107 -0
  74. ducta/api/routes/__init__.py +109 -0
  75. ducta/api/routes/auth.py +194 -0
  76. ducta/api/routes/certificates.py +154 -0
  77. ducta/api/routes/configs.py +129 -0
  78. ducta/api/routes/connect.py +95 -0
  79. ducta/api/routes/environments.py +51 -0
  80. ducta/api/routes/execution.py +829 -0
  81. ducta/api/routes/git.py +452 -0
  82. ducta/api/routes/health.py +59 -0
  83. ducta/api/routes/ingestion.py +238 -0
  84. ducta/api/routes/mlops.py +359 -0
  85. ducta/api/routes/nodes.py +384 -0
  86. ducta/api/routes/projects.py +590 -0
  87. ducta/api/routes/quality.py +257 -0
  88. ducta/api/routes/repository.py +316 -0
  89. ducta/api/routes/schedules.py +118 -0
  90. ducta/api/routes/templates.py +100 -0
  91. ducta/api/routes/terminal.py +209 -0
  92. ducta/api/routes/workspace.py +249 -0
  93. ducta/api/routes/workspace_files.py +91 -0
  94. ducta/api/services/__init__.py +37 -0
  95. ducta/api/services/config_cache.py +125 -0
  96. ducta/api/services/config_service.py +109 -0
  97. ducta/api/services/node_schema_service.py +266 -0
  98. ducta/api/services/node_service.py +194 -0
  99. ducta/api/services/project.py +429 -0
  100. ducta/api/source/__init__.py +24 -0
  101. ducta/api/source/models.py +59 -0
  102. ducta/api/source/resolver.py +419 -0
  103. ducta/api/utils/__init__.py +19 -0
  104. ducta/api/utils/audit.py +76 -0
  105. ducta/api/utils/git_utils.py +197 -0
  106. ducta/api/utils/logging.py +55 -0
  107. ducta/api/utils/platform_utils.py +83 -0
  108. ducta/api/utils/validators.py +132 -0
  109. ducta/api/workspace/__init__.py +23 -0
  110. ducta/api/workspace/loaders.py +267 -0
  111. ducta/api/workspace/manager.py +372 -0
  112. ducta/api/workspace/preflight.py +244 -0
  113. ducta/api/workspace/structure_detector.py +223 -0
  114. ducta/api/workspace/utils.py +122 -0
  115. ducta/check/README.md +168 -0
  116. ducta/check/__init__.py +186 -0
  117. ducta/check/advisor.py +110 -0
  118. ducta/check/checks/__init__.py +66 -0
  119. ducta/check/checks/business.py +312 -0
  120. ducta/check/checks/cross_table.py +214 -0
  121. ducta/check/checks/distribution.py +395 -0
  122. ducta/check/checks/structural.py +507 -0
  123. ducta/check/checks/temporal.py +383 -0
  124. ducta/check/core.py +509 -0
  125. ducta/check/engine.py +1130 -0
  126. ducta/check/gate.py +213 -0
  127. ducta/check/output_manager.py +237 -0
  128. ducta/check/profiles.py +126 -0
  129. ducta/check/serialization.py +125 -0
  130. ducta/check/service.py +379 -0
  131. ducta/check/storage.py +627 -0
  132. ducta/console/README.md +150 -0
  133. ducta/console/__init__.py +214 -0
  134. ducta/console/cli.py +388 -0
  135. ducta/console/commands/__init__.py +26 -0
  136. ducta/console/commands/certify_cmds.py +213 -0
  137. ducta/console/commands/config_cmds.py +315 -0
  138. ducta/console/commands/execution_cmds.py +503 -0
  139. ducta/console/commands/ingestion_cmds.py +45 -0
  140. ducta/console/commands/ingestion_setup.py +284 -0
  141. ducta/console/commands/quality_cmds.py +356 -0
  142. ducta/console/config.py +449 -0
  143. ducta/console/core.py +420 -0
  144. ducta/console/execution.py +288 -0
  145. ducta/console/mlops_commands.py +175 -0
  146. ducta/console/parser.py +658 -0
  147. ducta/console/template.py +1683 -0
  148. ducta/console/ui.py +169 -0
  149. ducta/console/ux/__init__.py +78 -0
  150. ducta/console/ux/error_analyzer.py +387 -0
  151. ducta/console/ux/formatters.py +278 -0
  152. ducta/console/ux/rich_logger.py +326 -0
  153. ducta/console/ux/schema_formatter.py +194 -0
  154. ducta/console/validation.py +447 -0
  155. ducta/console/wrapper.py +108 -0
  156. ducta/core/README.md +165 -0
  157. ducta/core/__init__.py +228 -0
  158. ducta/core/certificate.py +388 -0
  159. ducta/core/commands.py +377 -0
  160. ducta/core/dependency_inference.py +177 -0
  161. ducta/core/dependency_resolver.py +136 -0
  162. ducta/core/execution_context.py +42 -0
  163. ducta/core/executor.py +2029 -0
  164. ducta/core/import_security.py +280 -0
  165. ducta/core/ml_context.py +81 -0
  166. ducta/core/ml_node_validator.py +197 -0
  167. ducta/core/mlflow_node_executor.py +214 -0
  168. ducta/core/mlops_auto_config.py +177 -0
  169. ducta/core/mlops_integration.py +610 -0
  170. ducta/core/node_executor.py +1932 -0
  171. ducta/core/pipeline_dependency_resolver.py +111 -0
  172. ducta/core/pipeline_state.py +690 -0
  173. ducta/core/pipeline_validator.py +625 -0
  174. ducta/core/preflight.py +311 -0
  175. ducta/core/resilience.py +65 -0
  176. ducta/core/resource_manager.py +436 -0
  177. ducta/core/split_validator.py +195 -0
  178. ducta/core/storage/__init__.py +35 -0
  179. ducta/core/storage/base.py +220 -0
  180. ducta/core/sweep.py +66 -0
  181. ducta/core/utils.py +173 -0
  182. ducta/gate/README.md +235 -0
  183. ducta/gate/__init__.py +128 -0
  184. ducta/gate/base.py +139 -0
  185. ducta/gate/concurrency.py +74 -0
  186. ducta/gate/constants.py +68 -0
  187. ducta/gate/context_manager.py +121 -0
  188. ducta/gate/exceptions.py +64 -0
  189. ducta/gate/factories.py +128 -0
  190. ducta/gate/fingerprinting.py +71 -0
  191. ducta/gate/gateway/__init__.py +28 -0
  192. ducta/gate/gateway/circuit_breaker.py +101 -0
  193. ducta/gate/gateway/connector.py +399 -0
  194. ducta/gate/gateway/exceptions.py +28 -0
  195. ducta/gate/gateway/manager.py +453 -0
  196. ducta/gate/gateway/service.py +220 -0
  197. ducta/gate/gateway/spark_setup.py +91 -0
  198. ducta/gate/handoff.py +144 -0
  199. ducta/gate/input.py +360 -0
  200. ducta/gate/output/__init__.py +39 -0
  201. ducta/gate/output/dataframes.py +112 -0
  202. ducta/gate/output/manager.py +353 -0
  203. ducta/gate/output/paths.py +135 -0
  204. ducta/gate/output/unity_catalog.py +300 -0
  205. ducta/gate/readers.py +313 -0
  206. ducta/gate/sql.py +492 -0
  207. ducta/gate/validators.py +158 -0
  208. ducta/gate/writers.py +275 -0
  209. ducta/mlrun/README.md +159 -0
  210. ducta/mlrun/__init__.py +310 -0
  211. ducta/mlrun/base.py +532 -0
  212. ducta/mlrun/baseline.py +126 -0
  213. ducta/mlrun/cache.py +743 -0
  214. ducta/mlrun/concurrency.py +971 -0
  215. ducta/mlrun/config.py +703 -0
  216. ducta/mlrun/environment.py +156 -0
  217. ducta/mlrun/error_analysis.py +214 -0
  218. ducta/mlrun/events.py +1111 -0
  219. ducta/mlrun/exceptions.py +786 -0
  220. ducta/mlrun/experiment_tracking.py +1466 -0
  221. ducta/mlrun/fingerprint.py +186 -0
  222. ducta/mlrun/gc.py +110 -0
  223. ducta/mlrun/health.py +703 -0
  224. ducta/mlrun/hyperparams.py +644 -0
  225. ducta/mlrun/mlflow.py +1001 -0
  226. ducta/mlrun/model_registry.py +869 -0
  227. ducta/mlrun/persistence.py +132 -0
  228. ducta/mlrun/protocols.py +530 -0
  229. ducta/mlrun/resilience.py +626 -0
  230. ducta/mlrun/split.py +204 -0
  231. ducta/mlrun/storage.py +1253 -0
  232. ducta/mlrun/validators.py +579 -0
  233. ducta/setting/README.md +194 -0
  234. ducta/setting/__init__.py +175 -0
  235. ducta/setting/config_forms.py +212 -0
  236. ducta/setting/context_loader.py +109 -0
  237. ducta/setting/contexts.py +919 -0
  238. ducta/setting/environments.py +191 -0
  239. ducta/setting/exceptions.py +55 -0
  240. ducta/setting/interpolator.py +195 -0
  241. ducta/setting/layered_config.py +483 -0
  242. ducta/setting/loaders.py +270 -0
  243. ducta/setting/schemas.py +816 -0
  244. ducta/setting/session.py +528 -0
  245. ducta/setting/utils.py +34 -0
  246. ducta/setting/validators.py +522 -0
  247. ducta/stream/README.md +145 -0
  248. ducta/stream/__init__.py +115 -0
  249. ducta/stream/checkpoints.py +208 -0
  250. ducta/stream/constants.py +130 -0
  251. ducta/stream/context_utils.py +33 -0
  252. ducta/stream/exceptions.py +262 -0
  253. ducta/stream/pipeline_manager.py +1328 -0
  254. ducta/stream/progress_listener.py +175 -0
  255. ducta/stream/query_manager.py +942 -0
  256. ducta/stream/readers.py +495 -0
  257. ducta/stream/trigger_scheduler.py +247 -0
  258. ducta/stream/validators.py +1131 -0
  259. ducta/stream/writers.py +369 -0
  260. ducta/ui/dist/assets/ActionButton-DMlTJENg.js +1 -0
  261. ducta/ui/dist/assets/CodeEditor-Bv79wUIa.js +17 -0
  262. ducta/ui/dist/assets/CodeEditorModal-Bn6HIvo_.js +2 -0
  263. ducta/ui/dist/assets/FileTree-fy0K1hc5.js +11 -0
  264. ducta/ui/dist/assets/GitPage-BLZTifzs.js +1 -0
  265. ducta/ui/dist/assets/IconDeviceFloppy-DJx3woB0.js +6 -0
  266. ducta/ui/dist/assets/IconFile-BOwCvB1N.js +6 -0
  267. ducta/ui/dist/assets/IconGauge-C7_OWA3r.js +6 -0
  268. ducta/ui/dist/assets/IconRefresh--9MuA2Po.js +6 -0
  269. ducta/ui/dist/assets/IngestionPage-BejE_vmJ.js +16 -0
  270. ducta/ui/dist/assets/NodeCodePage-BDebkLnk.js +2 -0
  271. ducta/ui/dist/assets/PageHeader-CirL0F2V.js +1 -0
  272. ducta/ui/dist/assets/SchedulesPage-OXeqg9bk.js +6 -0
  273. ducta/ui/dist/assets/SlidePanel-DvSLs86R.js +1 -0
  274. ducta/ui/dist/assets/TerminalPage-CRnPkCpN.js +6 -0
  275. ducta/ui/dist/assets/index-5f-7Noni.js +6 -0
  276. ducta/ui/dist/assets/index-C3Z4BcMk.js +16 -0
  277. ducta/ui/dist/assets/index-D5LWWk6j.js +410 -0
  278. ducta/ui/dist/assets/index-DLMDRgWR.css +1 -0
  279. ducta/ui/dist/assets/index-DtQrMoBQ.js +6 -0
  280. ducta/ui/dist/assets/logs.worker-OYhZsjGZ.js +1 -0
  281. ducta/ui/dist/assets/vendor-query-Dhnd9gHa.js +1 -0
  282. ducta/ui/dist/assets/vendor-react-DX4KxU8z.js +50 -0
  283. ducta/ui/dist/assets/vendor-router-Bmx9pU3G.js +13 -0
  284. ducta/ui/dist/assets/vendor-state-y7_o29KJ.js +1 -0
  285. ducta/ui/dist/assets/vendor-utils-BxtLg8Md.js +37 -0
  286. ducta/ui/dist/index.html +22 -0
  287. ducta/ui/scripts/contrast-check.mjs +150 -0
  288. ducta/ui/scripts/ui-quality-check.mjs +62 -0
  289. ducta-0.1.0.dist-info/METADATA +358 -0
  290. ducta-0.1.0.dist-info/RECORD +293 -0
  291. ducta-0.1.0.dist-info/WHEEL +4 -0
  292. ducta-0.1.0.dist-info/entry_points.txt +3 -0
  293. 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