quantum-framework 0.18.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 (333) hide show
  1. quantum/__init__.py +1 -0
  2. quantum/cli/__init__.py +2 -0
  3. quantum/cli/admin.py +131 -0
  4. quantum/cli/check.py +223 -0
  5. quantum/cli/commands/__init__.py +15 -0
  6. quantum/cli/commands/build.py +414 -0
  7. quantum/cli/commands/dev.py +185 -0
  8. quantum/cli/commands/docs.py +329 -0
  9. quantum/cli/commands/lint.py +523 -0
  10. quantum/cli/commands/migrate.py +548 -0
  11. quantum/cli/commands/new.py +622 -0
  12. quantum/cli/commands/serve.py +190 -0
  13. quantum/cli/commands/test.py +193 -0
  14. quantum/cli/deploy.py +810 -0
  15. quantum/cli/hot_reload.py +951 -0
  16. quantum/cli/jobs.py +356 -0
  17. quantum/cli/mq.py +582 -0
  18. quantum/cli/pkg.py +390 -0
  19. quantum/cli/runner.py +565 -0
  20. quantum/cli/server_process.py +159 -0
  21. quantum/cli/utils.py +334 -0
  22. quantum/compiler/__init__.py +30 -0
  23. quantum/compiler/base_generator.py +367 -0
  24. quantum/compiler/cli.py +295 -0
  25. quantum/compiler/expression_transformer.py +444 -0
  26. quantum/compiler/javascript/__init__.py +10 -0
  27. quantum/compiler/javascript/generator.py +659 -0
  28. quantum/compiler/optimizer.py +270 -0
  29. quantum/compiler/python/__init__.py +10 -0
  30. quantum/compiler/python/generator.py +883 -0
  31. quantum/compiler/python/runtime.py +863 -0
  32. quantum/compiler/transpiler.py +330 -0
  33. quantum/core/__init__.py +3 -0
  34. quantum/core/ast_nodes.py +2619 -0
  35. quantum/core/config_env.py +54 -0
  36. quantum/core/docs_links.py +8 -0
  37. quantum/core/expression_diagnostics.py +87 -0
  38. quantum/core/expression_stdlib.py +208 -0
  39. quantum/core/expressions.py +677 -0
  40. quantum/core/features/agents/src/__init__.py +30 -0
  41. quantum/core/features/agents/src/ast_node.py +540 -0
  42. quantum/core/features/conditionals/src/__init__.py +8 -0
  43. quantum/core/features/conditionals/src/ast_node.py +68 -0
  44. quantum/core/features/data_import/src/__init__.py +0 -0
  45. quantum/core/features/data_import/src/ast_node.py +292 -0
  46. quantum/core/features/data_import/src/runtime.py +533 -0
  47. quantum/core/features/dump/src/__init__.py +12 -0
  48. quantum/core/features/dump/src/ast_node.py +106 -0
  49. quantum/core/features/dump/src/parser.py +61 -0
  50. quantum/core/features/dump/src/runtime.py +246 -0
  51. quantum/core/features/functions/src/__init__.py +8 -0
  52. quantum/core/features/functions/src/ast_node.py +149 -0
  53. quantum/core/features/game_engine_2d/src/__init__.py +19 -0
  54. quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
  55. quantum/core/features/game_engine_2d/src/parser.py +983 -0
  56. quantum/core/features/invocation/src/__init__.py +0 -0
  57. quantum/core/features/invocation/src/ast_node.py +147 -0
  58. quantum/core/features/invocation/src/runtime.py +332 -0
  59. quantum/core/features/knowledge_base/src/__init__.py +6 -0
  60. quantum/core/features/knowledge_base/src/ast_node.py +113 -0
  61. quantum/core/features/knowledge_base/src/parser.py +82 -0
  62. quantum/core/features/logging/src/__init__.py +12 -0
  63. quantum/core/features/logging/src/ast_node.py +111 -0
  64. quantum/core/features/logging/src/parser.py +50 -0
  65. quantum/core/features/logging/src/runtime.py +190 -0
  66. quantum/core/features/loops/src/__init__.py +8 -0
  67. quantum/core/features/loops/src/ast_node.py +60 -0
  68. quantum/core/features/query/src/__init__.py +0 -0
  69. quantum/core/features/query/src/query_validators.py +20 -0
  70. quantum/core/features/state_management/src/__init__.py +11 -0
  71. quantum/core/features/state_management/src/ast_node.py +121 -0
  72. quantum/core/features/terminal_engine/src/__init__.py +21 -0
  73. quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
  74. quantum/core/features/terminal_engine/src/parser.py +361 -0
  75. quantum/core/features/testing_engine/src/__init__.py +41 -0
  76. quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
  77. quantum/core/features/testing_engine/src/parser.py +604 -0
  78. quantum/core/features/theming/src/__init__.py +48 -0
  79. quantum/core/features/theming/src/ast_node.py +137 -0
  80. quantum/core/features/theming/src/presets.py +405 -0
  81. quantum/core/features/ui_engine/src/__init__.py +20 -0
  82. quantum/core/features/ui_engine/src/ast_nodes.py +1913 -0
  83. quantum/core/features/ui_engine/src/parser.py +1192 -0
  84. quantum/core/features/websocket/src/__init__.py +24 -0
  85. quantum/core/features/websocket/src/ast_node.py +247 -0
  86. quantum/core/guards.py +53 -0
  87. quantum/core/html_compat.py +299 -0
  88. quantum/core/parser.py +1551 -0
  89. quantum/core/parser_registry.py +213 -0
  90. quantum/core/parsers/__init__.py +76 -0
  91. quantum/core/parsers/ai/__init__.py +12 -0
  92. quantum/core/parsers/ai/agent_parser.py +106 -0
  93. quantum/core/parsers/ai/knowledge_parser.py +86 -0
  94. quantum/core/parsers/ai/llm_parser.py +78 -0
  95. quantum/core/parsers/ai/team_parser.py +88 -0
  96. quantum/core/parsers/base.py +322 -0
  97. quantum/core/parsers/composition/__init__.py +10 -0
  98. quantum/core/parsers/composition/import_parser.py +50 -0
  99. quantum/core/parsers/composition/slot_parser.py +48 -0
  100. quantum/core/parsers/control_flow/__init__.py +11 -0
  101. quantum/core/parsers/control_flow/if_parser.py +71 -0
  102. quantum/core/parsers/control_flow/loop_parser.py +105 -0
  103. quantum/core/parsers/control_flow/set_parser.py +93 -0
  104. quantum/core/parsers/data/__init__.py +12 -0
  105. quantum/core/parsers/data/data_parser.py +229 -0
  106. quantum/core/parsers/data/invoke_parser.py +133 -0
  107. quantum/core/parsers/data/query_parser.py +213 -0
  108. quantum/core/parsers/data/transaction_parser.py +86 -0
  109. quantum/core/parsers/events/__init__.py +9 -0
  110. quantum/core/parsers/events/dispatch_event_parser.py +44 -0
  111. quantum/core/parsers/forms/__init__.py +11 -0
  112. quantum/core/parsers/forms/action_parser.py +79 -0
  113. quantum/core/parsers/forms/flash_parser.py +34 -0
  114. quantum/core/parsers/forms/redirect_parser.py +33 -0
  115. quantum/core/parsers/functions/__init__.py +11 -0
  116. quantum/core/parsers/functions/function_parser.py +98 -0
  117. quantum/core/parsers/functions/param_parser.py +73 -0
  118. quantum/core/parsers/functions/return_parser.py +31 -0
  119. quantum/core/parsers/html/__init__.py +10 -0
  120. quantum/core/parsers/html/component_call_parser.py +130 -0
  121. quantum/core/parsers/html/html_parser.py +115 -0
  122. quantum/core/parsers/jobs/__init__.py +11 -0
  123. quantum/core/parsers/jobs/job_parser.py +71 -0
  124. quantum/core/parsers/jobs/schedule_parser.py +62 -0
  125. quantum/core/parsers/jobs/thread_parser.py +57 -0
  126. quantum/core/parsers/messaging/__init__.py +17 -0
  127. quantum/core/parsers/messaging/message_ack_parser.py +30 -0
  128. quantum/core/parsers/messaging/message_nack_parser.py +30 -0
  129. quantum/core/parsers/messaging/message_parser.py +114 -0
  130. quantum/core/parsers/messaging/queue_parser.py +61 -0
  131. quantum/core/parsers/messaging/websocket_parser.py +121 -0
  132. quantum/core/parsers/routing/__init__.py +9 -0
  133. quantum/core/parsers/routing/route_parser.py +41 -0
  134. quantum/core/parsers/scripting/__init__.py +12 -0
  135. quantum/core/parsers/scripting/pyclass_parser.py +62 -0
  136. quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
  137. quantum/core/parsers/scripting/pyimport_parser.py +52 -0
  138. quantum/core/parsers/scripting/python_parser.py +49 -0
  139. quantum/core/parsers/services/__init__.py +12 -0
  140. quantum/core/parsers/services/dump_parser.py +52 -0
  141. quantum/core/parsers/services/file_parser.py +48 -0
  142. quantum/core/parsers/services/log_parser.py +46 -0
  143. quantum/core/parsers/services/mail_parser.py +65 -0
  144. quantum/core/tiers.py +164 -0
  145. quantum/core/xml_lines.py +81 -0
  146. quantum/packages/__init__.py +28 -0
  147. quantum/packages/manager.py +413 -0
  148. quantum/packages/manifest.py +351 -0
  149. quantum/packages/registry.py +399 -0
  150. quantum/packages/resolver.py +336 -0
  151. quantum/plugins/__init__.py +33 -0
  152. quantum/plugins/hooks.py +329 -0
  153. quantum/plugins/loader.py +479 -0
  154. quantum/plugins/manifest.py +336 -0
  155. quantum/plugins/registry.py +371 -0
  156. quantum/runtime/__init__.py +25 -0
  157. quantum/runtime/action_handler.py +532 -0
  158. quantum/runtime/adapters/__init__.py +88 -0
  159. quantum/runtime/adapters/memory_adapter.py +690 -0
  160. quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
  161. quantum/runtime/adapters/redis_adapter.py +582 -0
  162. quantum/runtime/adapters/sqlite_adapter.py +414 -0
  163. quantum/runtime/agent_service.py +1133 -0
  164. quantum/runtime/ast_cache.py +506 -0
  165. quantum/runtime/auth_service.py +267 -0
  166. quantum/runtime/component.py +1048 -0
  167. quantum/runtime/component_composer.py +133 -0
  168. quantum/runtime/component_resolver.py +174 -0
  169. quantum/runtime/conditions.py +54 -0
  170. quantum/runtime/database_service.py +595 -0
  171. quantum/runtime/db_schema.py +100 -0
  172. quantum/runtime/dev_panel.py +178 -0
  173. quantum/runtime/email_service.py +162 -0
  174. quantum/runtime/error_handler.py +295 -0
  175. quantum/runtime/execution_context.py +283 -0
  176. quantum/runtime/executor_registry.py +178 -0
  177. quantum/runtime/executors/__init__.py +71 -0
  178. quantum/runtime/executors/ai/__init__.py +12 -0
  179. quantum/runtime/executors/ai/agent_executor.py +217 -0
  180. quantum/runtime/executors/ai/knowledge_executor.py +114 -0
  181. quantum/runtime/executors/ai/llm_executor.py +153 -0
  182. quantum/runtime/executors/ai/team_executor.py +171 -0
  183. quantum/runtime/executors/base.py +264 -0
  184. quantum/runtime/executors/control_flow/__init__.py +12 -0
  185. quantum/runtime/executors/control_flow/if_executor.py +98 -0
  186. quantum/runtime/executors/control_flow/loop_executor.py +308 -0
  187. quantum/runtime/executors/control_flow/redirect_executor.py +43 -0
  188. quantum/runtime/executors/control_flow/set_executor.py +464 -0
  189. quantum/runtime/executors/data/__init__.py +12 -0
  190. quantum/runtime/executors/data/data_executor.py +156 -0
  191. quantum/runtime/executors/data/invoke_executor.py +228 -0
  192. quantum/runtime/executors/data/query_executor.py +290 -0
  193. quantum/runtime/executors/data/transaction_executor.py +91 -0
  194. quantum/runtime/executors/jobs/__init__.py +11 -0
  195. quantum/runtime/executors/jobs/job_executor.py +190 -0
  196. quantum/runtime/executors/jobs/schedule_executor.py +132 -0
  197. quantum/runtime/executors/jobs/thread_executor.py +127 -0
  198. quantum/runtime/executors/messaging/__init__.py +17 -0
  199. quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
  200. quantum/runtime/executors/messaging/message_executor.py +174 -0
  201. quantum/runtime/executors/messaging/queue_executor.py +103 -0
  202. quantum/runtime/executors/messaging/websocket_executor.py +197 -0
  203. quantum/runtime/executors/scripting/__init__.py +11 -0
  204. quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
  205. quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
  206. quantum/runtime/executors/scripting/python_executor.py +249 -0
  207. quantum/runtime/executors/services/__init__.py +12 -0
  208. quantum/runtime/executors/services/dump_executor.py +72 -0
  209. quantum/runtime/executors/services/file_executor.py +89 -0
  210. quantum/runtime/executors/services/log_executor.py +77 -0
  211. quantum/runtime/executors/services/mail_executor.py +81 -0
  212. quantum/runtime/expression_cache.py +498 -0
  213. quantum/runtime/file_upload_service.py +326 -0
  214. quantum/runtime/function_registry.py +118 -0
  215. quantum/runtime/game_builder.py +166 -0
  216. quantum/runtime/game_code_generator.py +2371 -0
  217. quantum/runtime/game_templates.py +2006 -0
  218. quantum/runtime/godot_code_generator.py +4681 -0
  219. quantum/runtime/godot_templates.py +1449 -0
  220. quantum/runtime/job_executor.py +1599 -0
  221. quantum/runtime/knowledge_service.py +546 -0
  222. quantum/runtime/llm_cache.py +100 -0
  223. quantum/runtime/llm_providers.py +704 -0
  224. quantum/runtime/llm_service.py +287 -0
  225. quantum/runtime/local_server.py +28 -0
  226. quantum/runtime/logging_setup.py +141 -0
  227. quantum/runtime/message_broker.py +364 -0
  228. quantum/runtime/message_queue_service.py +571 -0
  229. quantum/runtime/param_validation.py +218 -0
  230. quantum/runtime/pypy_compat.py +317 -0
  231. quantum/runtime/python_bridge.py +698 -0
  232. quantum/runtime/query_validators.py +304 -0
  233. quantum/runtime/renderer.py +734 -0
  234. quantum/runtime/service_container.py +470 -0
  235. quantum/runtime/table_params.py +163 -0
  236. quantum/runtime/terminal_builder.py +76 -0
  237. quantum/runtime/terminal_code_generator.py +607 -0
  238. quantum/runtime/terminal_templates.py +243 -0
  239. quantum/runtime/testing_builder.py +77 -0
  240. quantum/runtime/testing_code_generator.py +833 -0
  241. quantum/runtime/testing_templates.py +85 -0
  242. quantum/runtime/ui_builder.py +222 -0
  243. quantum/runtime/ui_console.py +405 -0
  244. quantum/runtime/ui_desktop.py +56 -0
  245. quantum/runtime/ui_form_rules.py +193 -0
  246. quantum/runtime/ui_html_adapter.py +2567 -0
  247. quantum/runtime/ui_html_templates.py +2106 -0
  248. quantum/runtime/ui_mobile_adapter.py +1832 -0
  249. quantum/runtime/ui_mobile_templates.py +1003 -0
  250. quantum/runtime/ui_page_renderer.py +392 -0
  251. quantum/runtime/ui_pager.py +73 -0
  252. quantum/runtime/ui_table_edit.py +139 -0
  253. quantum/runtime/ui_textual_adapter.py +1866 -0
  254. quantum/runtime/ui_textual_templates.py +45 -0
  255. quantum/runtime/ui_tokens.py +465 -0
  256. quantum/runtime/ui_validator.py +365 -0
  257. quantum/runtime/ui_view.py +221 -0
  258. quantum/runtime/validators.py +256 -0
  259. quantum/runtime/web_server.py +2131 -0
  260. quantum/runtime/websocket_adapter.py +501 -0
  261. quantum/runtime/websocket_service.py +585 -0
  262. quantum/runtime/websocket_transport.py +289 -0
  263. quantum/runtime/wsgi.py +101 -0
  264. quantum/services.py +109 -0
  265. quantum/utils/__init__.py +1 -0
  266. quantum_admin/__init__.py +4 -0
  267. quantum_admin/assets/quantum-admin-connectors.js +123 -0
  268. quantum_admin/assets/quantum-admin.css +2058 -0
  269. quantum_admin/assets/quantum-source-highlight.js +106 -0
  270. quantum_admin/backend/__init__.py +4 -0
  271. quantum_admin/backend/audit_service.py +346 -0
  272. quantum_admin/backend/auth_service.py +394 -0
  273. quantum_admin/backend/component_discovery.py +766 -0
  274. quantum_admin/backend/config_generator.py +586 -0
  275. quantum_admin/backend/connector_service.py +1016 -0
  276. quantum_admin/backend/crud.py +557 -0
  277. quantum_admin/backend/database.py +226 -0
  278. quantum_admin/backend/db_setup_service.py +193 -0
  279. quantum_admin/backend/deploy_service.py +698 -0
  280. quantum_admin/backend/docker_service.py +422 -0
  281. quantum_admin/backend/environment_service.py +418 -0
  282. quantum_admin/backend/git_service.py +370 -0
  283. quantum_admin/backend/health_service.py +390 -0
  284. quantum_admin/backend/job_service.py +347 -0
  285. quantum_admin/backend/log_service.py +331 -0
  286. quantum_admin/backend/main.py +14426 -0
  287. quantum_admin/backend/models.py +986 -0
  288. quantum_admin/backend/pipeline_service.py +812 -0
  289. quantum_admin/backend/resource_manager.py +1118 -0
  290. quantum_admin/backend/schemas.py +419 -0
  291. quantum_admin/backend/secret_manager.py +277 -0
  292. quantum_admin/backend/settings_service.py +488 -0
  293. quantum_admin/backend/template_service.py +710 -0
  294. quantum_admin/backend/test_execution_service.py +364 -0
  295. quantum_admin/backend/test_generator.py +782 -0
  296. quantum_admin/backend/webhook_service.py +454 -0
  297. quantum_admin/backend/websocket_manager.py +360 -0
  298. quantum_admin/components/admin/_layout/AdminShell.q +121 -0
  299. quantum_admin/components/admin/agents.q +107 -0
  300. quantum_admin/components/admin/app/[name].q +558 -0
  301. quantum_admin/components/admin/applications.q +160 -0
  302. quantum_admin/components/admin/component/[...path].q +205 -0
  303. quantum_admin/components/admin/components.q +59 -0
  304. quantum_admin/components/admin/connectors.q +283 -0
  305. quantum_admin/components/admin/dashboard.q +86 -0
  306. quantum_admin/components/admin/database.q +95 -0
  307. quantum_admin/components/admin/features.q +69 -0
  308. quantum_admin/components/admin/index.q +12 -0
  309. quantum_admin/components/admin/jobs.q +75 -0
  310. quantum_admin/components/admin/login.q +65 -0
  311. quantum_admin/components/admin/logout.q +11 -0
  312. quantum_admin/components/admin/settings.q +174 -0
  313. quantum_admin/components/admin/source.q +45 -0
  314. quantum_admin/components/admin/tests.q +73 -0
  315. quantum_admin/run.py +13 -0
  316. quantum_admin/services/__init__.py +16 -0
  317. quantum_admin/services/_base.py +84 -0
  318. quantum_admin/services/_component_test_generator.py +205 -0
  319. quantum_admin/services/_yaml_editor.py +82 -0
  320. quantum_admin/services/apps.py +330 -0
  321. quantum_admin/services/auth.py +61 -0
  322. quantum_admin/services/components.py +210 -0
  323. quantum_admin/services/connectors.py +164 -0
  324. quantum_admin/services/projects.py +189 -0
  325. quantum_admin/services/repository.py +223 -0
  326. quantum_admin/services/settings.py +150 -0
  327. quantum_admin/services/yaml_import.py +118 -0
  328. quantum_framework-0.18.0.dist-info/METADATA +281 -0
  329. quantum_framework-0.18.0.dist-info/RECORD +333 -0
  330. quantum_framework-0.18.0.dist-info/WHEEL +5 -0
  331. quantum_framework-0.18.0.dist-info/entry_points.txt +2 -0
  332. quantum_framework-0.18.0.dist-info/licenses/LICENSE +21 -0
  333. quantum_framework-0.18.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,557 @@
1
+ """
2
+ CRUD operations for Quantum Admin
3
+ """
4
+ from sqlalchemy.orm import Session
5
+ from sqlalchemy.exc import IntegrityError
6
+ from typing import List, Optional
7
+ from datetime import datetime
8
+
9
+ from . import models
10
+
11
+
12
+ # ============================================================================
13
+ # PROJECT OPERATIONS
14
+ # ============================================================================
15
+
16
+ def get_projects(db: Session, skip: int = 0, limit: int = 100) -> List[models.Project]:
17
+ """Get all projects with pagination"""
18
+ return db.query(models.Project).offset(skip).limit(limit).all()
19
+
20
+
21
+ def get_project(db: Session, project_id: int) -> Optional[models.Project]:
22
+ """Get a single project by ID"""
23
+ return db.query(models.Project).filter(models.Project.id == project_id).first()
24
+
25
+
26
+ def get_project_by_name(db: Session, name: str) -> Optional[models.Project]:
27
+ """Get a project by name"""
28
+ return db.query(models.Project).filter(models.Project.name == name).first()
29
+
30
+
31
+ def create_project(db: Session, name: str, description: str = "") -> models.Project:
32
+ """Create a new project"""
33
+ project = models.Project(name=name, description=description)
34
+ db.add(project)
35
+ try:
36
+ db.commit()
37
+ db.refresh(project)
38
+ return project
39
+ except IntegrityError:
40
+ db.rollback()
41
+ raise ValueError(f"Project with name '{name}' already exists")
42
+
43
+
44
+ def update_project(
45
+ db: Session,
46
+ project_id: int,
47
+ name: Optional[str] = None,
48
+ description: Optional[str] = None,
49
+ status: Optional[str] = None,
50
+ source_path: Optional[str] = None,
51
+ git_url: Optional[str] = None,
52
+ git_branch: Optional[str] = None
53
+ ) -> Optional[models.Project]:
54
+ """Update an existing project"""
55
+ project = get_project(db, project_id)
56
+ if not project:
57
+ return None
58
+
59
+ if name is not None:
60
+ project.name = name
61
+ if description is not None:
62
+ project.description = description
63
+ if status is not None:
64
+ project.status = status
65
+ if source_path is not None:
66
+ project.source_path = source_path
67
+ if git_url is not None:
68
+ project.git_url = git_url
69
+ if git_branch is not None:
70
+ project.git_branch = git_branch
71
+
72
+ project.updated_at = datetime.utcnow()
73
+ db.commit()
74
+ db.refresh(project)
75
+ return project
76
+
77
+
78
+ # O que fazer com cada tabela que aponta para `projects.id` quando o projeto
79
+ # e apagado.
80
+ #
81
+ # `Project` declarava `cascade="all, delete-orphan"` para TRES relacoes —
82
+ # datasources, components, endpoints — e existem DEZENOVE tabelas com uma FK
83
+ # para projects.id. As outras dezesseis nao tinham relacao nenhuma, entao
84
+ # `db.delete(project)` nao as tocava:
85
+ #
86
+ # * as oito com project_id NOT NULL (test_runs, environment_variables,
87
+ # configuration_history, deployment_targets, deployments, environments,
88
+ # deployment_versions, application_logs) violavam a FK. Com o SQLite
89
+ # enforcando chaves estrangeiras isso e IntegrityError — HTTP 500 ao
90
+ # apagar um projeto que ja foi usado para qualquer coisa. Sem enforcar
91
+ # (o padrao do SQLite), as linhas ficavam apontando para um projeto que
92
+ # nao existe mais.
93
+ # * as oito com project_id NULL-avel ficavam orfas do mesmo jeito, e
94
+ # `secrets` e `port_allocations` continuavam segurando recursos de um
95
+ # projeto que ninguem consegue mais ver.
96
+ #
97
+ # 'delete' = a linha pertence ao projeto e vai com ele.
98
+ # 'null' = a linha e historico ou e compartilhavel; sobrevive sem dono.
99
+ _PROJECT_CHILD_POLICY = {
100
+ # NOT NULL: nao podem sobreviver
101
+ 'Datasource': ('project_id', 'delete'),
102
+ 'Component': ('project_id', 'delete'),
103
+ 'Endpoint': ('project_id', 'delete'),
104
+ 'TestRun': ('project_id', 'delete'),
105
+ 'EnvironmentVariable': ('project_id', 'delete'),
106
+ 'ConfigurationHistory': ('project_id', 'delete'),
107
+ 'DeploymentTarget': ('project_id', 'delete'),
108
+ 'Deployment': ('project_id', 'delete'),
109
+ 'Environment': ('project_id', 'delete'),
110
+ 'DeploymentVersion': ('project_id', 'delete'),
111
+ 'ApplicationLog': ('project_id', 'delete'),
112
+ # NULL-aveis que seguram recurso ou segredo: tambem vao.
113
+ # Um secret com project_id NULL viraria um secret GLOBAL, visivel para
114
+ # os outros projetos; uma porta alocada a ninguem nunca mais e liberada.
115
+ 'Secret': ('project_id', 'delete'),
116
+ 'PortAllocation': ('project_id', 'delete'),
117
+ 'ServiceRegistry': ('project_id', 'delete'),
118
+ # NULL-aveis que sao historico: ficam, sem dono. Apagar o log de
119
+ # auditoria de um projeto ao apagar o projeto e apagar exatamente o
120
+ # registro de que ele foi apagado.
121
+ 'AuditLog': ('project_id', 'null'),
122
+ 'WebhookEvent': ('project_id', 'null'),
123
+ 'HealthCheck': ('project_id', 'null'),
124
+ 'Incident': ('project_id', 'null'),
125
+ # Connector e explicitamente compartilhavel (tem `shared_with`).
126
+ 'Connector': ('owner_project_id', 'null'),
127
+ }
128
+
129
+
130
+ def _project_child_tables():
131
+ """Toda classe mapeada com uma FK para projects.id, com sua politica.
132
+
133
+ Descoberto por introspecao, e nao a partir de uma lista escrita a mao,
134
+ porque a lista escrita a mao foi exatamente o que ficou para tras: quem
135
+ adicionar uma tabela nova com project_id nao precisa lembrar de mexer
136
+ aqui — vai receber um erro dizendo que falta decidir.
137
+ """
138
+ encontradas = []
139
+ for classe in models.Base.registry.mappers:
140
+ modelo = classe.class_
141
+ for coluna in modelo.__table__.columns:
142
+ alvos = {fk.target_fullname for fk in coluna.foreign_keys}
143
+ if 'projects.id' not in alvos:
144
+ continue
145
+ politica = _PROJECT_CHILD_POLICY.get(modelo.__name__)
146
+ if politica is None or politica[0] != coluna.name:
147
+ raise RuntimeError(
148
+ f"{modelo.__name__}.{coluna.name} aponta para "
149
+ f"projects.id e nao tem politica de delecao em "
150
+ f"_PROJECT_CHILD_POLICY (crud.py). Decida se a linha "
151
+ f"vai junto ('delete') ou fica sem dono ('null')."
152
+ )
153
+ encontradas.append((modelo, coluna.name, politica[1]))
154
+
155
+ # Ordem: filho antes de pai. As tabelas do projeto tambem apontam umas
156
+ # para as outras (`deployments.target_id` -> `deployment_targets.id`),
157
+ # entao apagar em ordem arbitraria dava "FOREIGN KEY constraint failed"
158
+ # com as FKs enforcadas. `sorted_tables` vem em ordem de dependencia
159
+ # (pai primeiro); invertida, e a ordem de delecao.
160
+ ordem = {tabela.name: i for i, tabela in enumerate(
161
+ models.Base.metadata.sorted_tables)}
162
+ encontradas.sort(key=lambda t: -ordem.get(t[0].__tablename__, 0))
163
+ return encontradas
164
+
165
+
166
+ def delete_project(db: Session, project_id: int) -> bool:
167
+ """Delete a project and all its related data"""
168
+ project = get_project(db, project_id)
169
+ if not project:
170
+ return False
171
+
172
+ for modelo, coluna, acao in _project_child_tables():
173
+ filtro = getattr(modelo, coluna) == project_id
174
+ if acao == 'delete':
175
+ db.query(modelo).filter(filtro).delete(synchronize_session=False)
176
+ else:
177
+ db.query(modelo).filter(filtro).update(
178
+ {coluna: None}, synchronize_session=False)
179
+
180
+ db.delete(project)
181
+ db.commit()
182
+ return True
183
+
184
+
185
+ # ============================================================================
186
+ # DATASOURCE OPERATIONS
187
+ # ============================================================================
188
+
189
+ def get_datasources(db: Session, project_id: int) -> List[models.Datasource]:
190
+ """Get all datasources for a project"""
191
+ return db.query(models.Datasource).filter(
192
+ models.Datasource.project_id == project_id
193
+ ).all()
194
+
195
+
196
+ def get_datasource(db: Session, datasource_id: int) -> Optional[models.Datasource]:
197
+ """Get a single datasource by ID"""
198
+ return db.query(models.Datasource).filter(
199
+ models.Datasource.id == datasource_id
200
+ ).first()
201
+
202
+
203
+ def create_datasource(
204
+ db: Session,
205
+ project_id: int,
206
+ name: str,
207
+ type: str,
208
+ connection_type: str,
209
+ **kwargs
210
+ ) -> models.Datasource:
211
+ """Create a new datasource"""
212
+ datasource = models.Datasource(
213
+ project_id=project_id,
214
+ name=name,
215
+ type=type,
216
+ connection_type=connection_type,
217
+ **kwargs
218
+ )
219
+ db.add(datasource)
220
+ try:
221
+ db.commit()
222
+ db.refresh(datasource)
223
+ return datasource
224
+ except IntegrityError:
225
+ db.rollback()
226
+ raise ValueError(f"Datasource with name '{name}' already exists in this project")
227
+
228
+
229
+ def update_datasource(
230
+ db: Session,
231
+ datasource_id: int,
232
+ **kwargs
233
+ ) -> Optional[models.Datasource]:
234
+ """Update a datasource"""
235
+ datasource = get_datasource(db, datasource_id)
236
+ if not datasource:
237
+ return None
238
+
239
+ for key, value in kwargs.items():
240
+ if hasattr(datasource, key) and value is not None:
241
+ setattr(datasource, key, value)
242
+
243
+ datasource.updated_at = datetime.utcnow()
244
+ db.commit()
245
+ db.refresh(datasource)
246
+ return datasource
247
+
248
+
249
+ def delete_datasource(db: Session, datasource_id: int) -> bool:
250
+ """Delete a datasource"""
251
+ datasource = get_datasource(db, datasource_id)
252
+ if not datasource:
253
+ return False
254
+
255
+ db.delete(datasource)
256
+ db.commit()
257
+ return True
258
+
259
+
260
+ # ============================================================================
261
+ # COMPONENT OPERATIONS
262
+ # ============================================================================
263
+
264
+ def get_components(db: Session, project_id: int) -> List[models.Component]:
265
+ """Get all components for a project"""
266
+ return db.query(models.Component).filter(
267
+ models.Component.project_id == project_id
268
+ ).all()
269
+
270
+
271
+ def get_component(db: Session, component_id: int) -> Optional[models.Component]:
272
+ """Get a single component by ID"""
273
+ return db.query(models.Component).filter(
274
+ models.Component.id == component_id
275
+ ).first()
276
+
277
+
278
+ def create_or_update_component(
279
+ db: Session,
280
+ project_id: int,
281
+ name: str,
282
+ file_path: str,
283
+ status: str = "unknown",
284
+ error_message: Optional[str] = None
285
+ ) -> models.Component:
286
+ """Create or update a component (upsert)"""
287
+ component = db.query(models.Component).filter(
288
+ models.Component.project_id == project_id,
289
+ models.Component.name == name
290
+ ).first()
291
+
292
+ if component:
293
+ # Update existing
294
+ component.file_path = file_path
295
+ component.status = status
296
+ component.error_message = error_message
297
+ component.last_compiled = datetime.utcnow()
298
+ else:
299
+ # Create new
300
+ component = models.Component(
301
+ project_id=project_id,
302
+ name=name,
303
+ file_path=file_path,
304
+ status=status,
305
+ error_message=error_message,
306
+ last_compiled=datetime.utcnow()
307
+ )
308
+ db.add(component)
309
+
310
+ db.commit()
311
+ db.refresh(component)
312
+ return component
313
+
314
+
315
+ # ============================================================================
316
+ # ENDPOINT OPERATIONS
317
+ # ============================================================================
318
+
319
+ def get_endpoints(db: Session, project_id: int) -> List[models.Endpoint]:
320
+ """Get all endpoints for a project"""
321
+ return db.query(models.Endpoint).filter(
322
+ models.Endpoint.project_id == project_id
323
+ ).all()
324
+
325
+
326
+ def create_or_update_endpoint(
327
+ db: Session,
328
+ project_id: int,
329
+ method: str,
330
+ path: str,
331
+ function_name: str,
332
+ component_id: Optional[int] = None,
333
+ description: Optional[str] = None
334
+ ) -> models.Endpoint:
335
+ """Create or update an endpoint (upsert)"""
336
+ endpoint = db.query(models.Endpoint).filter(
337
+ models.Endpoint.project_id == project_id,
338
+ models.Endpoint.method == method,
339
+ models.Endpoint.path == path
340
+ ).first()
341
+
342
+ if endpoint:
343
+ # Update existing
344
+ endpoint.function_name = function_name
345
+ endpoint.component_id = component_id
346
+ endpoint.description = description
347
+ else:
348
+ # Create new
349
+ endpoint = models.Endpoint(
350
+ project_id=project_id,
351
+ method=method,
352
+ path=path,
353
+ function_name=function_name,
354
+ component_id=component_id,
355
+ description=description
356
+ )
357
+ db.add(endpoint)
358
+
359
+ db.commit()
360
+ db.refresh(endpoint)
361
+ return endpoint
362
+
363
+
364
+ # ============================================================================
365
+ # TEST RUN OPERATIONS
366
+ # ============================================================================
367
+
368
+ def get_test_runs(
369
+ db: Session,
370
+ project_id: int,
371
+ skip: int = 0,
372
+ limit: int = 100
373
+ ) -> List[models.TestRun]:
374
+ """Get all test runs for a project with pagination"""
375
+ return db.query(models.TestRun).filter(
376
+ models.TestRun.project_id == project_id
377
+ ).order_by(models.TestRun.started_at.desc()).offset(skip).limit(limit).all()
378
+
379
+
380
+ def get_test_run(db: Session, test_run_id: int) -> Optional[models.TestRun]:
381
+ """Get a single test run by ID with all test results"""
382
+ return db.query(models.TestRun).filter(
383
+ models.TestRun.id == test_run_id
384
+ ).first()
385
+
386
+
387
+ def get_latest_test_run(db: Session, project_id: int) -> Optional[models.TestRun]:
388
+ """Get the latest test run for a project"""
389
+ return db.query(models.TestRun).filter(
390
+ models.TestRun.project_id == project_id
391
+ ).order_by(models.TestRun.started_at.desc()).first()
392
+
393
+
394
+ def delete_test_run(db: Session, test_run_id: int) -> bool:
395
+ """Delete a test run and all its results"""
396
+ test_run = get_test_run(db, test_run_id)
397
+ if not test_run:
398
+ return False
399
+
400
+ db.delete(test_run)
401
+ db.commit()
402
+ return True
403
+
404
+
405
+ # ============================================================================
406
+ # TEST RESULT OPERATIONS
407
+ # ============================================================================
408
+
409
+ def get_test_results(db: Session, test_run_id: int) -> List[models.TestResult]:
410
+ """Get all test results for a test run"""
411
+ return db.query(models.TestResult).filter(
412
+ models.TestResult.test_run_id == test_run_id
413
+ ).all()
414
+
415
+
416
+ def get_failed_test_results(db: Session, test_run_id: int) -> List[models.TestResult]:
417
+ """Get only failed test results for a test run"""
418
+ return db.query(models.TestResult).filter(
419
+ models.TestResult.test_run_id == test_run_id,
420
+ models.TestResult.status == 'failed'
421
+ ).all()
422
+
423
+
424
+ # ============================================================================
425
+ # ENVIRONMENT VARIABLE OPERATIONS
426
+ # ============================================================================
427
+
428
+ def get_environment_variables(
429
+ db: Session,
430
+ project_id: int
431
+ ) -> List[models.EnvironmentVariable]:
432
+ """Get all environment variables for a project"""
433
+ return db.query(models.EnvironmentVariable).filter(
434
+ models.EnvironmentVariable.project_id == project_id
435
+ ).order_by(models.EnvironmentVariable.key).all()
436
+
437
+
438
+ def get_environment_variable(
439
+ db: Session,
440
+ project_id: int,
441
+ key: str
442
+ ) -> Optional[models.EnvironmentVariable]:
443
+ """Get a specific environment variable by key"""
444
+ return db.query(models.EnvironmentVariable).filter(
445
+ models.EnvironmentVariable.project_id == project_id,
446
+ models.EnvironmentVariable.key == key
447
+ ).first()
448
+
449
+
450
+ def create_environment_variable(
451
+ db: Session,
452
+ project_id: int,
453
+ key: str,
454
+ value_encrypted: str,
455
+ description: Optional[str] = None,
456
+ is_secret: bool = False
457
+ ) -> models.EnvironmentVariable:
458
+ """Create a new environment variable"""
459
+ env_var = models.EnvironmentVariable(
460
+ project_id=project_id,
461
+ key=key,
462
+ value_encrypted=value_encrypted,
463
+ description=description,
464
+ is_secret=is_secret
465
+ )
466
+ db.add(env_var)
467
+ try:
468
+ db.commit()
469
+ db.refresh(env_var)
470
+ return env_var
471
+ except IntegrityError:
472
+ db.rollback()
473
+ raise ValueError(f"Environment variable with key '{key}' already exists in this project")
474
+
475
+
476
+ def update_environment_variable(
477
+ db: Session,
478
+ project_id: int,
479
+ key: str,
480
+ value_encrypted: Optional[str] = None,
481
+ description: Optional[str] = None,
482
+ is_secret: Optional[bool] = None
483
+ ) -> Optional[models.EnvironmentVariable]:
484
+ """Update an environment variable"""
485
+ env_var = get_environment_variable(db, project_id, key)
486
+ if not env_var:
487
+ return None
488
+
489
+ if value_encrypted is not None:
490
+ env_var.value_encrypted = value_encrypted
491
+ if description is not None:
492
+ env_var.description = description
493
+ if is_secret is not None:
494
+ env_var.is_secret = is_secret
495
+
496
+ env_var.updated_at = datetime.utcnow()
497
+ db.commit()
498
+ db.refresh(env_var)
499
+ return env_var
500
+
501
+
502
+ def delete_environment_variable(
503
+ db: Session,
504
+ project_id: int,
505
+ key: str
506
+ ) -> bool:
507
+ """Delete an environment variable"""
508
+ env_var = get_environment_variable(db, project_id, key)
509
+ if not env_var:
510
+ return False
511
+
512
+ db.delete(env_var)
513
+ db.commit()
514
+ return True
515
+
516
+
517
+ # ============================================================================
518
+ # CONFIGURATION HISTORY OPERATIONS
519
+ # ============================================================================
520
+
521
+ def get_configuration_history(
522
+ db: Session,
523
+ project_id: int,
524
+ limit: int = 50
525
+ ) -> List[models.ConfigurationHistory]:
526
+ """Get configuration history for a project"""
527
+ return db.query(models.ConfigurationHistory).filter(
528
+ models.ConfigurationHistory.project_id == project_id
529
+ ).order_by(models.ConfigurationHistory.changed_at.desc()).limit(limit).all()
530
+
531
+
532
+ def create_configuration_history(
533
+ db: Session,
534
+ project_id: int,
535
+ changes_json: str,
536
+ snapshot_json: str,
537
+ changed_by: str = "system"
538
+ ) -> models.ConfigurationHistory:
539
+ """Create a configuration history entry"""
540
+ # Get the next version number
541
+ latest = db.query(models.ConfigurationHistory).filter(
542
+ models.ConfigurationHistory.project_id == project_id
543
+ ).order_by(models.ConfigurationHistory.version.desc()).first()
544
+
545
+ version = (latest.version + 1) if latest else 1
546
+
547
+ history = models.ConfigurationHistory(
548
+ project_id=project_id,
549
+ version=version,
550
+ changed_by=changed_by,
551
+ changes_json=changes_json,
552
+ snapshot_json=snapshot_json
553
+ )
554
+ db.add(history)
555
+ db.commit()
556
+ db.refresh(history)
557
+ return history