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.
- quantum/__init__.py +1 -0
- quantum/cli/__init__.py +2 -0
- quantum/cli/admin.py +131 -0
- quantum/cli/check.py +223 -0
- quantum/cli/commands/__init__.py +15 -0
- quantum/cli/commands/build.py +414 -0
- quantum/cli/commands/dev.py +185 -0
- quantum/cli/commands/docs.py +329 -0
- quantum/cli/commands/lint.py +523 -0
- quantum/cli/commands/migrate.py +548 -0
- quantum/cli/commands/new.py +622 -0
- quantum/cli/commands/serve.py +190 -0
- quantum/cli/commands/test.py +193 -0
- quantum/cli/deploy.py +810 -0
- quantum/cli/hot_reload.py +951 -0
- quantum/cli/jobs.py +356 -0
- quantum/cli/mq.py +582 -0
- quantum/cli/pkg.py +390 -0
- quantum/cli/runner.py +565 -0
- quantum/cli/server_process.py +159 -0
- quantum/cli/utils.py +334 -0
- quantum/compiler/__init__.py +30 -0
- quantum/compiler/base_generator.py +367 -0
- quantum/compiler/cli.py +295 -0
- quantum/compiler/expression_transformer.py +444 -0
- quantum/compiler/javascript/__init__.py +10 -0
- quantum/compiler/javascript/generator.py +659 -0
- quantum/compiler/optimizer.py +270 -0
- quantum/compiler/python/__init__.py +10 -0
- quantum/compiler/python/generator.py +883 -0
- quantum/compiler/python/runtime.py +863 -0
- quantum/compiler/transpiler.py +330 -0
- quantum/core/__init__.py +3 -0
- quantum/core/ast_nodes.py +2619 -0
- quantum/core/config_env.py +54 -0
- quantum/core/docs_links.py +8 -0
- quantum/core/expression_diagnostics.py +87 -0
- quantum/core/expression_stdlib.py +208 -0
- quantum/core/expressions.py +677 -0
- quantum/core/features/agents/src/__init__.py +30 -0
- quantum/core/features/agents/src/ast_node.py +540 -0
- quantum/core/features/conditionals/src/__init__.py +8 -0
- quantum/core/features/conditionals/src/ast_node.py +68 -0
- quantum/core/features/data_import/src/__init__.py +0 -0
- quantum/core/features/data_import/src/ast_node.py +292 -0
- quantum/core/features/data_import/src/runtime.py +533 -0
- quantum/core/features/dump/src/__init__.py +12 -0
- quantum/core/features/dump/src/ast_node.py +106 -0
- quantum/core/features/dump/src/parser.py +61 -0
- quantum/core/features/dump/src/runtime.py +246 -0
- quantum/core/features/functions/src/__init__.py +8 -0
- quantum/core/features/functions/src/ast_node.py +149 -0
- quantum/core/features/game_engine_2d/src/__init__.py +19 -0
- quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
- quantum/core/features/game_engine_2d/src/parser.py +983 -0
- quantum/core/features/invocation/src/__init__.py +0 -0
- quantum/core/features/invocation/src/ast_node.py +147 -0
- quantum/core/features/invocation/src/runtime.py +332 -0
- quantum/core/features/knowledge_base/src/__init__.py +6 -0
- quantum/core/features/knowledge_base/src/ast_node.py +113 -0
- quantum/core/features/knowledge_base/src/parser.py +82 -0
- quantum/core/features/logging/src/__init__.py +12 -0
- quantum/core/features/logging/src/ast_node.py +111 -0
- quantum/core/features/logging/src/parser.py +50 -0
- quantum/core/features/logging/src/runtime.py +190 -0
- quantum/core/features/loops/src/__init__.py +8 -0
- quantum/core/features/loops/src/ast_node.py +60 -0
- quantum/core/features/query/src/__init__.py +0 -0
- quantum/core/features/query/src/query_validators.py +20 -0
- quantum/core/features/state_management/src/__init__.py +11 -0
- quantum/core/features/state_management/src/ast_node.py +121 -0
- quantum/core/features/terminal_engine/src/__init__.py +21 -0
- quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
- quantum/core/features/terminal_engine/src/parser.py +361 -0
- quantum/core/features/testing_engine/src/__init__.py +41 -0
- quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
- quantum/core/features/testing_engine/src/parser.py +604 -0
- quantum/core/features/theming/src/__init__.py +48 -0
- quantum/core/features/theming/src/ast_node.py +137 -0
- quantum/core/features/theming/src/presets.py +405 -0
- quantum/core/features/ui_engine/src/__init__.py +20 -0
- quantum/core/features/ui_engine/src/ast_nodes.py +1913 -0
- quantum/core/features/ui_engine/src/parser.py +1192 -0
- quantum/core/features/websocket/src/__init__.py +24 -0
- quantum/core/features/websocket/src/ast_node.py +247 -0
- quantum/core/guards.py +53 -0
- quantum/core/html_compat.py +299 -0
- quantum/core/parser.py +1551 -0
- quantum/core/parser_registry.py +213 -0
- quantum/core/parsers/__init__.py +76 -0
- quantum/core/parsers/ai/__init__.py +12 -0
- quantum/core/parsers/ai/agent_parser.py +106 -0
- quantum/core/parsers/ai/knowledge_parser.py +86 -0
- quantum/core/parsers/ai/llm_parser.py +78 -0
- quantum/core/parsers/ai/team_parser.py +88 -0
- quantum/core/parsers/base.py +322 -0
- quantum/core/parsers/composition/__init__.py +10 -0
- quantum/core/parsers/composition/import_parser.py +50 -0
- quantum/core/parsers/composition/slot_parser.py +48 -0
- quantum/core/parsers/control_flow/__init__.py +11 -0
- quantum/core/parsers/control_flow/if_parser.py +71 -0
- quantum/core/parsers/control_flow/loop_parser.py +105 -0
- quantum/core/parsers/control_flow/set_parser.py +93 -0
- quantum/core/parsers/data/__init__.py +12 -0
- quantum/core/parsers/data/data_parser.py +229 -0
- quantum/core/parsers/data/invoke_parser.py +133 -0
- quantum/core/parsers/data/query_parser.py +213 -0
- quantum/core/parsers/data/transaction_parser.py +86 -0
- quantum/core/parsers/events/__init__.py +9 -0
- quantum/core/parsers/events/dispatch_event_parser.py +44 -0
- quantum/core/parsers/forms/__init__.py +11 -0
- quantum/core/parsers/forms/action_parser.py +79 -0
- quantum/core/parsers/forms/flash_parser.py +34 -0
- quantum/core/parsers/forms/redirect_parser.py +33 -0
- quantum/core/parsers/functions/__init__.py +11 -0
- quantum/core/parsers/functions/function_parser.py +98 -0
- quantum/core/parsers/functions/param_parser.py +73 -0
- quantum/core/parsers/functions/return_parser.py +31 -0
- quantum/core/parsers/html/__init__.py +10 -0
- quantum/core/parsers/html/component_call_parser.py +130 -0
- quantum/core/parsers/html/html_parser.py +115 -0
- quantum/core/parsers/jobs/__init__.py +11 -0
- quantum/core/parsers/jobs/job_parser.py +71 -0
- quantum/core/parsers/jobs/schedule_parser.py +62 -0
- quantum/core/parsers/jobs/thread_parser.py +57 -0
- quantum/core/parsers/messaging/__init__.py +17 -0
- quantum/core/parsers/messaging/message_ack_parser.py +30 -0
- quantum/core/parsers/messaging/message_nack_parser.py +30 -0
- quantum/core/parsers/messaging/message_parser.py +114 -0
- quantum/core/parsers/messaging/queue_parser.py +61 -0
- quantum/core/parsers/messaging/websocket_parser.py +121 -0
- quantum/core/parsers/routing/__init__.py +9 -0
- quantum/core/parsers/routing/route_parser.py +41 -0
- quantum/core/parsers/scripting/__init__.py +12 -0
- quantum/core/parsers/scripting/pyclass_parser.py +62 -0
- quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
- quantum/core/parsers/scripting/pyimport_parser.py +52 -0
- quantum/core/parsers/scripting/python_parser.py +49 -0
- quantum/core/parsers/services/__init__.py +12 -0
- quantum/core/parsers/services/dump_parser.py +52 -0
- quantum/core/parsers/services/file_parser.py +48 -0
- quantum/core/parsers/services/log_parser.py +46 -0
- quantum/core/parsers/services/mail_parser.py +65 -0
- quantum/core/tiers.py +164 -0
- quantum/core/xml_lines.py +81 -0
- quantum/packages/__init__.py +28 -0
- quantum/packages/manager.py +413 -0
- quantum/packages/manifest.py +351 -0
- quantum/packages/registry.py +399 -0
- quantum/packages/resolver.py +336 -0
- quantum/plugins/__init__.py +33 -0
- quantum/plugins/hooks.py +329 -0
- quantum/plugins/loader.py +479 -0
- quantum/plugins/manifest.py +336 -0
- quantum/plugins/registry.py +371 -0
- quantum/runtime/__init__.py +25 -0
- quantum/runtime/action_handler.py +532 -0
- quantum/runtime/adapters/__init__.py +88 -0
- quantum/runtime/adapters/memory_adapter.py +690 -0
- quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
- quantum/runtime/adapters/redis_adapter.py +582 -0
- quantum/runtime/adapters/sqlite_adapter.py +414 -0
- quantum/runtime/agent_service.py +1133 -0
- quantum/runtime/ast_cache.py +506 -0
- quantum/runtime/auth_service.py +267 -0
- quantum/runtime/component.py +1048 -0
- quantum/runtime/component_composer.py +133 -0
- quantum/runtime/component_resolver.py +174 -0
- quantum/runtime/conditions.py +54 -0
- quantum/runtime/database_service.py +595 -0
- quantum/runtime/db_schema.py +100 -0
- quantum/runtime/dev_panel.py +178 -0
- quantum/runtime/email_service.py +162 -0
- quantum/runtime/error_handler.py +295 -0
- quantum/runtime/execution_context.py +283 -0
- quantum/runtime/executor_registry.py +178 -0
- quantum/runtime/executors/__init__.py +71 -0
- quantum/runtime/executors/ai/__init__.py +12 -0
- quantum/runtime/executors/ai/agent_executor.py +217 -0
- quantum/runtime/executors/ai/knowledge_executor.py +114 -0
- quantum/runtime/executors/ai/llm_executor.py +153 -0
- quantum/runtime/executors/ai/team_executor.py +171 -0
- quantum/runtime/executors/base.py +264 -0
- quantum/runtime/executors/control_flow/__init__.py +12 -0
- quantum/runtime/executors/control_flow/if_executor.py +98 -0
- quantum/runtime/executors/control_flow/loop_executor.py +308 -0
- quantum/runtime/executors/control_flow/redirect_executor.py +43 -0
- quantum/runtime/executors/control_flow/set_executor.py +464 -0
- quantum/runtime/executors/data/__init__.py +12 -0
- quantum/runtime/executors/data/data_executor.py +156 -0
- quantum/runtime/executors/data/invoke_executor.py +228 -0
- quantum/runtime/executors/data/query_executor.py +290 -0
- quantum/runtime/executors/data/transaction_executor.py +91 -0
- quantum/runtime/executors/jobs/__init__.py +11 -0
- quantum/runtime/executors/jobs/job_executor.py +190 -0
- quantum/runtime/executors/jobs/schedule_executor.py +132 -0
- quantum/runtime/executors/jobs/thread_executor.py +127 -0
- quantum/runtime/executors/messaging/__init__.py +17 -0
- quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
- quantum/runtime/executors/messaging/message_executor.py +174 -0
- quantum/runtime/executors/messaging/queue_executor.py +103 -0
- quantum/runtime/executors/messaging/websocket_executor.py +197 -0
- quantum/runtime/executors/scripting/__init__.py +11 -0
- quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
- quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
- quantum/runtime/executors/scripting/python_executor.py +249 -0
- quantum/runtime/executors/services/__init__.py +12 -0
- quantum/runtime/executors/services/dump_executor.py +72 -0
- quantum/runtime/executors/services/file_executor.py +89 -0
- quantum/runtime/executors/services/log_executor.py +77 -0
- quantum/runtime/executors/services/mail_executor.py +81 -0
- quantum/runtime/expression_cache.py +498 -0
- quantum/runtime/file_upload_service.py +326 -0
- quantum/runtime/function_registry.py +118 -0
- quantum/runtime/game_builder.py +166 -0
- quantum/runtime/game_code_generator.py +2371 -0
- quantum/runtime/game_templates.py +2006 -0
- quantum/runtime/godot_code_generator.py +4681 -0
- quantum/runtime/godot_templates.py +1449 -0
- quantum/runtime/job_executor.py +1599 -0
- quantum/runtime/knowledge_service.py +546 -0
- quantum/runtime/llm_cache.py +100 -0
- quantum/runtime/llm_providers.py +704 -0
- quantum/runtime/llm_service.py +287 -0
- quantum/runtime/local_server.py +28 -0
- quantum/runtime/logging_setup.py +141 -0
- quantum/runtime/message_broker.py +364 -0
- quantum/runtime/message_queue_service.py +571 -0
- quantum/runtime/param_validation.py +218 -0
- quantum/runtime/pypy_compat.py +317 -0
- quantum/runtime/python_bridge.py +698 -0
- quantum/runtime/query_validators.py +304 -0
- quantum/runtime/renderer.py +734 -0
- quantum/runtime/service_container.py +470 -0
- quantum/runtime/table_params.py +163 -0
- quantum/runtime/terminal_builder.py +76 -0
- quantum/runtime/terminal_code_generator.py +607 -0
- quantum/runtime/terminal_templates.py +243 -0
- quantum/runtime/testing_builder.py +77 -0
- quantum/runtime/testing_code_generator.py +833 -0
- quantum/runtime/testing_templates.py +85 -0
- quantum/runtime/ui_builder.py +222 -0
- quantum/runtime/ui_console.py +405 -0
- quantum/runtime/ui_desktop.py +56 -0
- quantum/runtime/ui_form_rules.py +193 -0
- quantum/runtime/ui_html_adapter.py +2567 -0
- quantum/runtime/ui_html_templates.py +2106 -0
- quantum/runtime/ui_mobile_adapter.py +1832 -0
- quantum/runtime/ui_mobile_templates.py +1003 -0
- quantum/runtime/ui_page_renderer.py +392 -0
- quantum/runtime/ui_pager.py +73 -0
- quantum/runtime/ui_table_edit.py +139 -0
- quantum/runtime/ui_textual_adapter.py +1866 -0
- quantum/runtime/ui_textual_templates.py +45 -0
- quantum/runtime/ui_tokens.py +465 -0
- quantum/runtime/ui_validator.py +365 -0
- quantum/runtime/ui_view.py +221 -0
- quantum/runtime/validators.py +256 -0
- quantum/runtime/web_server.py +2131 -0
- quantum/runtime/websocket_adapter.py +501 -0
- quantum/runtime/websocket_service.py +585 -0
- quantum/runtime/websocket_transport.py +289 -0
- quantum/runtime/wsgi.py +101 -0
- quantum/services.py +109 -0
- quantum/utils/__init__.py +1 -0
- quantum_admin/__init__.py +4 -0
- quantum_admin/assets/quantum-admin-connectors.js +123 -0
- quantum_admin/assets/quantum-admin.css +2058 -0
- quantum_admin/assets/quantum-source-highlight.js +106 -0
- quantum_admin/backend/__init__.py +4 -0
- quantum_admin/backend/audit_service.py +346 -0
- quantum_admin/backend/auth_service.py +394 -0
- quantum_admin/backend/component_discovery.py +766 -0
- quantum_admin/backend/config_generator.py +586 -0
- quantum_admin/backend/connector_service.py +1016 -0
- quantum_admin/backend/crud.py +557 -0
- quantum_admin/backend/database.py +226 -0
- quantum_admin/backend/db_setup_service.py +193 -0
- quantum_admin/backend/deploy_service.py +698 -0
- quantum_admin/backend/docker_service.py +422 -0
- quantum_admin/backend/environment_service.py +418 -0
- quantum_admin/backend/git_service.py +370 -0
- quantum_admin/backend/health_service.py +390 -0
- quantum_admin/backend/job_service.py +347 -0
- quantum_admin/backend/log_service.py +331 -0
- quantum_admin/backend/main.py +14426 -0
- quantum_admin/backend/models.py +986 -0
- quantum_admin/backend/pipeline_service.py +812 -0
- quantum_admin/backend/resource_manager.py +1118 -0
- quantum_admin/backend/schemas.py +419 -0
- quantum_admin/backend/secret_manager.py +277 -0
- quantum_admin/backend/settings_service.py +488 -0
- quantum_admin/backend/template_service.py +710 -0
- quantum_admin/backend/test_execution_service.py +364 -0
- quantum_admin/backend/test_generator.py +782 -0
- quantum_admin/backend/webhook_service.py +454 -0
- quantum_admin/backend/websocket_manager.py +360 -0
- quantum_admin/components/admin/_layout/AdminShell.q +121 -0
- quantum_admin/components/admin/agents.q +107 -0
- quantum_admin/components/admin/app/[name].q +558 -0
- quantum_admin/components/admin/applications.q +160 -0
- quantum_admin/components/admin/component/[...path].q +205 -0
- quantum_admin/components/admin/components.q +59 -0
- quantum_admin/components/admin/connectors.q +283 -0
- quantum_admin/components/admin/dashboard.q +86 -0
- quantum_admin/components/admin/database.q +95 -0
- quantum_admin/components/admin/features.q +69 -0
- quantum_admin/components/admin/index.q +12 -0
- quantum_admin/components/admin/jobs.q +75 -0
- quantum_admin/components/admin/login.q +65 -0
- quantum_admin/components/admin/logout.q +11 -0
- quantum_admin/components/admin/settings.q +174 -0
- quantum_admin/components/admin/source.q +45 -0
- quantum_admin/components/admin/tests.q +73 -0
- quantum_admin/run.py +13 -0
- quantum_admin/services/__init__.py +16 -0
- quantum_admin/services/_base.py +84 -0
- quantum_admin/services/_component_test_generator.py +205 -0
- quantum_admin/services/_yaml_editor.py +82 -0
- quantum_admin/services/apps.py +330 -0
- quantum_admin/services/auth.py +61 -0
- quantum_admin/services/components.py +210 -0
- quantum_admin/services/connectors.py +164 -0
- quantum_admin/services/projects.py +189 -0
- quantum_admin/services/repository.py +223 -0
- quantum_admin/services/settings.py +150 -0
- quantum_admin/services/yaml_import.py +118 -0
- quantum_framework-0.18.0.dist-info/METADATA +281 -0
- quantum_framework-0.18.0.dist-info/RECORD +333 -0
- quantum_framework-0.18.0.dist-info/WHEEL +5 -0
- quantum_framework-0.18.0.dist-info/entry_points.txt +2 -0
- quantum_framework-0.18.0.dist-info/licenses/LICENSE +21 -0
- 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
|