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,766 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Component Discovery Service - Automatically discover and parse Quantum components
|
|
3
|
+
|
|
4
|
+
Features:
|
|
5
|
+
- Scan project directories for .q files
|
|
6
|
+
- Parse .q files to extract metadata
|
|
7
|
+
- Extract endpoints, functions, queries
|
|
8
|
+
- Validate component syntax
|
|
9
|
+
- Track component dependencies
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import logging
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Dict, List, Optional, Any, Tuple
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from xml.etree import ElementTree as ET
|
|
20
|
+
from sqlalchemy.orm import Session
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
from .models import Project, Component, Endpoint, ComponentTest
|
|
24
|
+
except ImportError:
|
|
25
|
+
from models import Project, Component, Endpoint, ComponentTest
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class DiscoveredFunction:
|
|
32
|
+
"""Represents a function discovered in a component"""
|
|
33
|
+
name: str
|
|
34
|
+
params: List[str] = field(default_factory=list)
|
|
35
|
+
return_type: Optional[str] = None
|
|
36
|
+
description: Optional[str] = None
|
|
37
|
+
line_number: int = 0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class DiscoveredEndpoint:
|
|
42
|
+
"""Represents an endpoint discovered in a component"""
|
|
43
|
+
method: str # GET, POST, PUT, DELETE
|
|
44
|
+
path: str
|
|
45
|
+
function_name: str
|
|
46
|
+
description: Optional[str] = None
|
|
47
|
+
line_number: int = 0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class DiscoveredQuery:
|
|
52
|
+
"""Represents a database query discovered in a component"""
|
|
53
|
+
name: str
|
|
54
|
+
datasource: Optional[str] = None
|
|
55
|
+
query_type: str = "select" # select, insert, update, delete, exec
|
|
56
|
+
line_number: int = 0
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class DiscoveredDependency:
|
|
61
|
+
"""Represents a component dependency"""
|
|
62
|
+
component_name: str
|
|
63
|
+
import_path: Optional[str] = None
|
|
64
|
+
line_number: int = 0
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class ComponentMetadata:
|
|
69
|
+
"""Complete metadata for a discovered component"""
|
|
70
|
+
name: str
|
|
71
|
+
file_path: str
|
|
72
|
+
title: Optional[str] = None
|
|
73
|
+
description: Optional[str] = None
|
|
74
|
+
|
|
75
|
+
# Discovered elements
|
|
76
|
+
functions: List[DiscoveredFunction] = field(default_factory=list)
|
|
77
|
+
endpoints: List[DiscoveredEndpoint] = field(default_factory=list)
|
|
78
|
+
queries: List[DiscoveredQuery] = field(default_factory=list)
|
|
79
|
+
dependencies: List[DiscoveredDependency] = field(default_factory=list)
|
|
80
|
+
|
|
81
|
+
# Variables and state
|
|
82
|
+
variables: List[str] = field(default_factory=list)
|
|
83
|
+
|
|
84
|
+
# Validation
|
|
85
|
+
is_valid: bool = True
|
|
86
|
+
errors: List[str] = field(default_factory=list)
|
|
87
|
+
warnings: List[str] = field(default_factory=list)
|
|
88
|
+
|
|
89
|
+
# Stats
|
|
90
|
+
line_count: int = 0
|
|
91
|
+
last_modified: Optional[datetime] = None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class QuantumParser:
|
|
95
|
+
"""Parser for Quantum .q files"""
|
|
96
|
+
|
|
97
|
+
# Quantum namespace
|
|
98
|
+
Q_NAMESPACE = "q"
|
|
99
|
+
|
|
100
|
+
# Tag patterns for extraction
|
|
101
|
+
COMPONENT_PATTERN = re.compile(r'<q:component\s+name=["\']([^"\']+)["\']', re.IGNORECASE)
|
|
102
|
+
FUNCTION_PATTERN = re.compile(r'<q:function\s+name=["\']([^"\']+)["\']([^>]*)>', re.IGNORECASE)
|
|
103
|
+
ENDPOINT_PATTERN = re.compile(r'<q:endpoint\s+([^>]+)>', re.IGNORECASE)
|
|
104
|
+
QUERY_PATTERN = re.compile(r'<q:query\s+name=["\']([^"\']+)["\']([^>]*)>', re.IGNORECASE)
|
|
105
|
+
SET_PATTERN = re.compile(r'<q:set\s+var=["\']([^"\']+)["\']', re.IGNORECASE)
|
|
106
|
+
IMPORT_PATTERN = re.compile(r'<q:import\s+component=["\']([^"\']+)["\']', re.IGNORECASE)
|
|
107
|
+
INCLUDE_PATTERN = re.compile(r'<q:include\s+file=["\']([^"\']+)["\']', re.IGNORECASE)
|
|
108
|
+
|
|
109
|
+
# HTTP method patterns
|
|
110
|
+
HTTP_GET_PATTERN = re.compile(r'<q:http-get\s+path=["\']([^"\']+)["\']([^>]*)>', re.IGNORECASE)
|
|
111
|
+
HTTP_POST_PATTERN = re.compile(r'<q:http-post\s+path=["\']([^"\']+)["\']([^>]*)>', re.IGNORECASE)
|
|
112
|
+
HTTP_PUT_PATTERN = re.compile(r'<q:http-put\s+path=["\']([^"\']+)["\']([^>]*)>', re.IGNORECASE)
|
|
113
|
+
HTTP_DELETE_PATTERN = re.compile(r'<q:http-delete\s+path=["\']([^"\']+)["\']([^>]*)>', re.IGNORECASE)
|
|
114
|
+
|
|
115
|
+
@classmethod
|
|
116
|
+
def parse_file(cls, file_path: str) -> ComponentMetadata:
|
|
117
|
+
"""Parse a .q file and extract metadata"""
|
|
118
|
+
path = Path(file_path)
|
|
119
|
+
|
|
120
|
+
if not path.exists():
|
|
121
|
+
return ComponentMetadata(
|
|
122
|
+
name=path.stem,
|
|
123
|
+
file_path=str(path),
|
|
124
|
+
is_valid=False,
|
|
125
|
+
errors=["File not found"]
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
content = path.read_text(encoding='utf-8')
|
|
130
|
+
lines = content.split('\n')
|
|
131
|
+
line_count = len(lines)
|
|
132
|
+
last_modified = datetime.fromtimestamp(path.stat().st_mtime)
|
|
133
|
+
|
|
134
|
+
# Extract component name
|
|
135
|
+
name_match = cls.COMPONENT_PATTERN.search(content)
|
|
136
|
+
component_name = name_match.group(1) if name_match else path.stem
|
|
137
|
+
|
|
138
|
+
metadata = ComponentMetadata(
|
|
139
|
+
name=component_name,
|
|
140
|
+
file_path=str(path),
|
|
141
|
+
line_count=line_count,
|
|
142
|
+
last_modified=last_modified
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Parse functions
|
|
146
|
+
metadata.functions = cls._parse_functions(content)
|
|
147
|
+
|
|
148
|
+
# Parse endpoints (both explicit and HTTP method tags)
|
|
149
|
+
metadata.endpoints = cls._parse_endpoints(content)
|
|
150
|
+
|
|
151
|
+
# Parse queries
|
|
152
|
+
metadata.queries = cls._parse_queries(content)
|
|
153
|
+
|
|
154
|
+
# Parse dependencies
|
|
155
|
+
metadata.dependencies = cls._parse_dependencies(content)
|
|
156
|
+
|
|
157
|
+
# Parse variables
|
|
158
|
+
metadata.variables = cls._parse_variables(content)
|
|
159
|
+
|
|
160
|
+
# Validate XML structure
|
|
161
|
+
validation_errors = cls._validate_xml(content)
|
|
162
|
+
if validation_errors:
|
|
163
|
+
metadata.warnings.extend(validation_errors)
|
|
164
|
+
|
|
165
|
+
return metadata
|
|
166
|
+
|
|
167
|
+
except Exception as e:
|
|
168
|
+
logger.error(f"Error parsing {file_path}: {e}")
|
|
169
|
+
return ComponentMetadata(
|
|
170
|
+
name=path.stem,
|
|
171
|
+
file_path=str(path),
|
|
172
|
+
is_valid=False,
|
|
173
|
+
errors=[str(e)]
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
@classmethod
|
|
177
|
+
def _parse_functions(cls, content: str) -> List[DiscoveredFunction]:
|
|
178
|
+
"""Extract function definitions from content"""
|
|
179
|
+
functions = []
|
|
180
|
+
lines = content.split('\n')
|
|
181
|
+
|
|
182
|
+
for i, line in enumerate(lines):
|
|
183
|
+
match = cls.FUNCTION_PATTERN.search(line)
|
|
184
|
+
if match:
|
|
185
|
+
name = match.group(1)
|
|
186
|
+
attrs = match.group(2)
|
|
187
|
+
|
|
188
|
+
# Extract parameters
|
|
189
|
+
params = []
|
|
190
|
+
params_match = re.search(r'params=["\']([^"\']+)["\']', attrs)
|
|
191
|
+
if params_match:
|
|
192
|
+
params = [p.strip() for p in params_match.group(1).split(',')]
|
|
193
|
+
|
|
194
|
+
# Extract return type
|
|
195
|
+
return_type = None
|
|
196
|
+
return_match = re.search(r'returns=["\']([^"\']+)["\']', attrs)
|
|
197
|
+
if return_match:
|
|
198
|
+
return_type = return_match.group(1)
|
|
199
|
+
|
|
200
|
+
functions.append(DiscoveredFunction(
|
|
201
|
+
name=name,
|
|
202
|
+
params=params,
|
|
203
|
+
return_type=return_type,
|
|
204
|
+
line_number=i + 1
|
|
205
|
+
))
|
|
206
|
+
|
|
207
|
+
return functions
|
|
208
|
+
|
|
209
|
+
@classmethod
|
|
210
|
+
def _parse_endpoints(cls, content: str) -> List[DiscoveredEndpoint]:
|
|
211
|
+
"""Extract endpoint definitions from content"""
|
|
212
|
+
endpoints = []
|
|
213
|
+
lines = content.split('\n')
|
|
214
|
+
|
|
215
|
+
# Parse explicit q:endpoint tags
|
|
216
|
+
for i, line in enumerate(lines):
|
|
217
|
+
match = cls.ENDPOINT_PATTERN.search(line)
|
|
218
|
+
if match:
|
|
219
|
+
attrs = match.group(1)
|
|
220
|
+
|
|
221
|
+
method_match = re.search(r'method=["\']([^"\']+)["\']', attrs)
|
|
222
|
+
path_match = re.search(r'path=["\']([^"\']+)["\']', attrs)
|
|
223
|
+
func_match = re.search(r'function=["\']([^"\']+)["\']', attrs)
|
|
224
|
+
|
|
225
|
+
if path_match:
|
|
226
|
+
endpoints.append(DiscoveredEndpoint(
|
|
227
|
+
method=method_match.group(1).upper() if method_match else "GET",
|
|
228
|
+
path=path_match.group(1),
|
|
229
|
+
function_name=func_match.group(1) if func_match else "",
|
|
230
|
+
line_number=i + 1
|
|
231
|
+
))
|
|
232
|
+
|
|
233
|
+
# Parse HTTP method tags (q:http-get, q:http-post, etc.)
|
|
234
|
+
http_patterns = [
|
|
235
|
+
(cls.HTTP_GET_PATTERN, "GET"),
|
|
236
|
+
(cls.HTTP_POST_PATTERN, "POST"),
|
|
237
|
+
(cls.HTTP_PUT_PATTERN, "PUT"),
|
|
238
|
+
(cls.HTTP_DELETE_PATTERN, "DELETE"),
|
|
239
|
+
]
|
|
240
|
+
|
|
241
|
+
for i, line in enumerate(lines):
|
|
242
|
+
for pattern, method in http_patterns:
|
|
243
|
+
match = pattern.search(line)
|
|
244
|
+
if match:
|
|
245
|
+
path = match.group(1)
|
|
246
|
+
attrs = match.group(2)
|
|
247
|
+
|
|
248
|
+
func_match = re.search(r'function=["\']([^"\']+)["\']', attrs)
|
|
249
|
+
|
|
250
|
+
endpoints.append(DiscoveredEndpoint(
|
|
251
|
+
method=method,
|
|
252
|
+
path=path,
|
|
253
|
+
function_name=func_match.group(1) if func_match else "",
|
|
254
|
+
line_number=i + 1
|
|
255
|
+
))
|
|
256
|
+
|
|
257
|
+
return endpoints
|
|
258
|
+
|
|
259
|
+
@classmethod
|
|
260
|
+
def _parse_queries(cls, content: str) -> List[DiscoveredQuery]:
|
|
261
|
+
"""Extract query definitions from content"""
|
|
262
|
+
queries = []
|
|
263
|
+
lines = content.split('\n')
|
|
264
|
+
|
|
265
|
+
for i, line in enumerate(lines):
|
|
266
|
+
match = cls.QUERY_PATTERN.search(line)
|
|
267
|
+
if match:
|
|
268
|
+
name = match.group(1)
|
|
269
|
+
attrs = match.group(2)
|
|
270
|
+
|
|
271
|
+
datasource = None
|
|
272
|
+
ds_match = re.search(r'datasource=["\']([^"\']+)["\']', attrs)
|
|
273
|
+
if ds_match:
|
|
274
|
+
datasource = ds_match.group(1)
|
|
275
|
+
|
|
276
|
+
# Determine query type from content
|
|
277
|
+
query_type = "select"
|
|
278
|
+
if re.search(r'type=["\'](\w+)["\']', attrs):
|
|
279
|
+
type_match = re.search(r'type=["\'](\w+)["\']', attrs)
|
|
280
|
+
query_type = type_match.group(1).lower()
|
|
281
|
+
|
|
282
|
+
queries.append(DiscoveredQuery(
|
|
283
|
+
name=name,
|
|
284
|
+
datasource=datasource,
|
|
285
|
+
query_type=query_type,
|
|
286
|
+
line_number=i + 1
|
|
287
|
+
))
|
|
288
|
+
|
|
289
|
+
return queries
|
|
290
|
+
|
|
291
|
+
@classmethod
|
|
292
|
+
def _parse_dependencies(cls, content: str) -> List[DiscoveredDependency]:
|
|
293
|
+
"""Extract component dependencies from content"""
|
|
294
|
+
dependencies = []
|
|
295
|
+
lines = content.split('\n')
|
|
296
|
+
|
|
297
|
+
for i, line in enumerate(lines):
|
|
298
|
+
# Check q:import
|
|
299
|
+
import_match = cls.IMPORT_PATTERN.search(line)
|
|
300
|
+
if import_match:
|
|
301
|
+
dependencies.append(DiscoveredDependency(
|
|
302
|
+
component_name=import_match.group(1),
|
|
303
|
+
line_number=i + 1
|
|
304
|
+
))
|
|
305
|
+
|
|
306
|
+
# Check q:include
|
|
307
|
+
include_match = cls.INCLUDE_PATTERN.search(line)
|
|
308
|
+
if include_match:
|
|
309
|
+
dependencies.append(DiscoveredDependency(
|
|
310
|
+
component_name=include_match.group(1),
|
|
311
|
+
import_path=include_match.group(1),
|
|
312
|
+
line_number=i + 1
|
|
313
|
+
))
|
|
314
|
+
|
|
315
|
+
return dependencies
|
|
316
|
+
|
|
317
|
+
@classmethod
|
|
318
|
+
def _parse_variables(cls, content: str) -> List[str]:
|
|
319
|
+
"""Extract variable names from content"""
|
|
320
|
+
variables = set()
|
|
321
|
+
|
|
322
|
+
for match in cls.SET_PATTERN.finditer(content):
|
|
323
|
+
variables.add(match.group(1))
|
|
324
|
+
|
|
325
|
+
return list(variables)
|
|
326
|
+
|
|
327
|
+
@classmethod
|
|
328
|
+
def _validate_xml(cls, content: str) -> List[str]:
|
|
329
|
+
"""Validate XML structure and return any errors"""
|
|
330
|
+
errors = []
|
|
331
|
+
|
|
332
|
+
# A MESMA normalizacao que o parser do framework faz.
|
|
333
|
+
#
|
|
334
|
+
# Sem ela, este validador reprovava arquivos .q que o proprio Quantum
|
|
335
|
+
# carrega sem reclamar: `<input required>`, `<br>`, `Forms & Actions`,
|
|
336
|
+
# `condition="n < 0"`, o corpo Python de um <q:python>. O componente
|
|
337
|
+
# aparecia na interface do admin marcado com "XML parse error" e o
|
|
338
|
+
# autor nao tinha o que consertar — o arquivo estava certo.
|
|
339
|
+
try:
|
|
340
|
+
from quantum.core.html_compat import normalise_html
|
|
341
|
+
content = normalise_html(content)
|
|
342
|
+
except ImportError: # pragma: no cover
|
|
343
|
+
pass
|
|
344
|
+
|
|
345
|
+
# A declaracao XML tem de sair antes de embrulhar. Um arquivo que
|
|
346
|
+
# comeca com `<?xml version="1.0"?>` — dezenas dos entregues — virava
|
|
347
|
+
# `<root ...><?xml ...?>`, e o erro reportado era "XML or text
|
|
348
|
+
# declaration not at start of entity", que descreve o embrulho, nao o
|
|
349
|
+
# arquivo.
|
|
350
|
+
content = re.sub(r'^\s*<\?xml[^?]*\?>\s*', '', content)
|
|
351
|
+
|
|
352
|
+
# E todo prefixo usado no documento precisa estar declarado no
|
|
353
|
+
# embrulho. So `q:` estava, entao `qg:` (jogos), `qt:` (terminal),
|
|
354
|
+
# `ui:` e `qtest:` davam "unbound prefix" — de novo um defeito do
|
|
355
|
+
# embrulho, num arquivo correto. Declarar o que o documento usa
|
|
356
|
+
# mantem os erros ESTRUTURAIS visiveis (tag nao fechada, token
|
|
357
|
+
# invalido) e elimina os inventados.
|
|
358
|
+
prefixos = sorted(set(re.findall(r'</?([A-Za-z_][\w.-]*):', content)))
|
|
359
|
+
ns = ' '.join(f"xmlns:{p}='quantum-{p}'" for p in prefixos) or ''
|
|
360
|
+
wrapped = f"<root {ns}>{content}</root>"
|
|
361
|
+
|
|
362
|
+
try:
|
|
363
|
+
ET.fromstring(wrapped)
|
|
364
|
+
except ET.ParseError as e:
|
|
365
|
+
errors.append(f"XML parse error: {e}")
|
|
366
|
+
|
|
367
|
+
return errors
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
class ComponentDiscoveryService:
|
|
371
|
+
"""Service for discovering components in a project"""
|
|
372
|
+
|
|
373
|
+
def __init__(self, db: Session):
|
|
374
|
+
self.db = db
|
|
375
|
+
self.parser = QuantumParser()
|
|
376
|
+
|
|
377
|
+
def discover_components(
|
|
378
|
+
self,
|
|
379
|
+
project_id: int,
|
|
380
|
+
base_path: Optional[str] = None,
|
|
381
|
+
include_patterns: Optional[List[str]] = None,
|
|
382
|
+
exclude_patterns: Optional[List[str]] = None
|
|
383
|
+
) -> List[ComponentMetadata]:
|
|
384
|
+
"""
|
|
385
|
+
Discover all components in a project
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
project_id: Project ID
|
|
389
|
+
base_path: Base path to scan (defaults to project path)
|
|
390
|
+
include_patterns: Glob patterns to include
|
|
391
|
+
exclude_patterns: Glob patterns to exclude
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
List of discovered component metadata
|
|
395
|
+
"""
|
|
396
|
+
project = self.db.query(Project).filter(Project.id == project_id).first()
|
|
397
|
+
if not project:
|
|
398
|
+
raise ValueError(f"Project {project_id} not found")
|
|
399
|
+
|
|
400
|
+
# Determine base path - try project settings or use current directory
|
|
401
|
+
if not base_path:
|
|
402
|
+
# Project model may not have path field - use current directory as fallback
|
|
403
|
+
base_path = getattr(project, 'path', None) or getattr(project, 'root_path', None) or "."
|
|
404
|
+
|
|
405
|
+
if not os.path.isabs(base_path):
|
|
406
|
+
base_path = os.path.abspath(base_path)
|
|
407
|
+
|
|
408
|
+
if not os.path.exists(base_path):
|
|
409
|
+
logger.warning(f"Project path does not exist: {base_path}")
|
|
410
|
+
return []
|
|
411
|
+
|
|
412
|
+
# Default patterns
|
|
413
|
+
if include_patterns is None:
|
|
414
|
+
include_patterns = ["**/*.q", "**/*.quantum"]
|
|
415
|
+
|
|
416
|
+
if exclude_patterns is None:
|
|
417
|
+
exclude_patterns = ["**/node_modules/**", "**/.git/**", "**/venv/**", "**/__pycache__/**"]
|
|
418
|
+
|
|
419
|
+
# Find all .q files
|
|
420
|
+
discovered = []
|
|
421
|
+
base = Path(base_path)
|
|
422
|
+
|
|
423
|
+
for pattern in include_patterns:
|
|
424
|
+
for file_path in base.glob(pattern):
|
|
425
|
+
# Check exclusions
|
|
426
|
+
rel_path = str(file_path.relative_to(base))
|
|
427
|
+
excluded = False
|
|
428
|
+
|
|
429
|
+
for exclude in exclude_patterns:
|
|
430
|
+
if file_path.match(exclude):
|
|
431
|
+
excluded = True
|
|
432
|
+
break
|
|
433
|
+
|
|
434
|
+
if not excluded:
|
|
435
|
+
metadata = self.parser.parse_file(str(file_path))
|
|
436
|
+
discovered.append(metadata)
|
|
437
|
+
|
|
438
|
+
return discovered
|
|
439
|
+
|
|
440
|
+
def sync_components(
|
|
441
|
+
self,
|
|
442
|
+
project_id: int,
|
|
443
|
+
base_path: Optional[str] = None
|
|
444
|
+
) -> Tuple[int, int, int]:
|
|
445
|
+
"""
|
|
446
|
+
Sync discovered components with database
|
|
447
|
+
|
|
448
|
+
Returns:
|
|
449
|
+
Tuple of (created, updated, deleted) counts
|
|
450
|
+
"""
|
|
451
|
+
# Discover components
|
|
452
|
+
discovered = self.discover_components(project_id, base_path)
|
|
453
|
+
discovered_paths = {m.file_path for m in discovered}
|
|
454
|
+
|
|
455
|
+
# Get existing components
|
|
456
|
+
existing = self.db.query(Component).filter(
|
|
457
|
+
Component.project_id == project_id
|
|
458
|
+
).all()
|
|
459
|
+
existing_map = {c.file_path: c for c in existing}
|
|
460
|
+
|
|
461
|
+
created = 0
|
|
462
|
+
updated = 0
|
|
463
|
+
deleted = 0
|
|
464
|
+
|
|
465
|
+
# Process discovered components
|
|
466
|
+
for metadata in discovered:
|
|
467
|
+
if metadata.file_path in existing_map:
|
|
468
|
+
# Update existing
|
|
469
|
+
component = existing_map[metadata.file_path]
|
|
470
|
+
component.name = metadata.name
|
|
471
|
+
component.status = "active" if metadata.is_valid else "error"
|
|
472
|
+
component.error_message = "; ".join(metadata.errors) if metadata.errors else None
|
|
473
|
+
component.last_compiled = metadata.last_modified
|
|
474
|
+
|
|
475
|
+
# Update counters
|
|
476
|
+
component.function_count = len(metadata.functions)
|
|
477
|
+
component.endpoint_count = len(metadata.endpoints)
|
|
478
|
+
component.query_count = len(metadata.queries)
|
|
479
|
+
|
|
480
|
+
updated += 1
|
|
481
|
+
|
|
482
|
+
# Sync endpoints
|
|
483
|
+
self._sync_endpoints(component.id, project_id, metadata.endpoints)
|
|
484
|
+
else:
|
|
485
|
+
# Create new
|
|
486
|
+
component = Component(
|
|
487
|
+
project_id=project_id,
|
|
488
|
+
name=metadata.name,
|
|
489
|
+
file_path=metadata.file_path,
|
|
490
|
+
status="active" if metadata.is_valid else "error",
|
|
491
|
+
error_message="; ".join(metadata.errors) if metadata.errors else None,
|
|
492
|
+
last_compiled=metadata.last_modified,
|
|
493
|
+
function_count=len(metadata.functions),
|
|
494
|
+
endpoint_count=len(metadata.endpoints),
|
|
495
|
+
query_count=len(metadata.queries)
|
|
496
|
+
)
|
|
497
|
+
self.db.add(component)
|
|
498
|
+
self.db.flush() # Get component ID
|
|
499
|
+
created += 1
|
|
500
|
+
|
|
501
|
+
# Create endpoints
|
|
502
|
+
self._sync_endpoints(component.id, project_id, metadata.endpoints)
|
|
503
|
+
|
|
504
|
+
# Remove deleted components
|
|
505
|
+
for file_path, component in existing_map.items():
|
|
506
|
+
if file_path not in discovered_paths:
|
|
507
|
+
# Delete endpoints first
|
|
508
|
+
self.db.query(Endpoint).filter(
|
|
509
|
+
Endpoint.component_id == component.id
|
|
510
|
+
).delete()
|
|
511
|
+
self.db.delete(component)
|
|
512
|
+
deleted += 1
|
|
513
|
+
|
|
514
|
+
self.db.commit()
|
|
515
|
+
|
|
516
|
+
return created, updated, deleted
|
|
517
|
+
|
|
518
|
+
def _sync_endpoints(
|
|
519
|
+
self,
|
|
520
|
+
component_id: int,
|
|
521
|
+
project_id: int,
|
|
522
|
+
endpoints: List[DiscoveredEndpoint]
|
|
523
|
+
):
|
|
524
|
+
"""Sync endpoints for a component"""
|
|
525
|
+
# Delete existing endpoints for this component
|
|
526
|
+
self.db.query(Endpoint).filter(
|
|
527
|
+
Endpoint.component_id == component_id
|
|
528
|
+
).delete()
|
|
529
|
+
|
|
530
|
+
# Create new endpoints
|
|
531
|
+
for ep in endpoints:
|
|
532
|
+
endpoint = Endpoint(
|
|
533
|
+
project_id=project_id,
|
|
534
|
+
component_id=component_id,
|
|
535
|
+
method=ep.method,
|
|
536
|
+
path=ep.path,
|
|
537
|
+
function_name=ep.function_name,
|
|
538
|
+
description=ep.description
|
|
539
|
+
)
|
|
540
|
+
self.db.add(endpoint)
|
|
541
|
+
|
|
542
|
+
def get_component_details(
|
|
543
|
+
self,
|
|
544
|
+
project_id: int,
|
|
545
|
+
component_id: int
|
|
546
|
+
) -> Optional[ComponentMetadata]:
|
|
547
|
+
"""Get detailed metadata for a specific component"""
|
|
548
|
+
component = self.db.query(Component).filter(
|
|
549
|
+
Component.id == component_id,
|
|
550
|
+
Component.project_id == project_id
|
|
551
|
+
).first()
|
|
552
|
+
|
|
553
|
+
if not component:
|
|
554
|
+
return None
|
|
555
|
+
|
|
556
|
+
return self.parser.parse_file(component.file_path)
|
|
557
|
+
|
|
558
|
+
def validate_component(
|
|
559
|
+
self,
|
|
560
|
+
file_path: str
|
|
561
|
+
) -> Tuple[bool, List[str], List[str]]:
|
|
562
|
+
"""
|
|
563
|
+
Validate a component file
|
|
564
|
+
|
|
565
|
+
Returns:
|
|
566
|
+
Tuple of (is_valid, errors, warnings)
|
|
567
|
+
"""
|
|
568
|
+
metadata = self.parser.parse_file(file_path)
|
|
569
|
+
return metadata.is_valid, metadata.errors, metadata.warnings
|
|
570
|
+
|
|
571
|
+
def get_dependency_graph(
|
|
572
|
+
self,
|
|
573
|
+
project_id: int,
|
|
574
|
+
base_path: Optional[str] = None
|
|
575
|
+
) -> Dict[str, List[str]]:
|
|
576
|
+
"""
|
|
577
|
+
Build dependency graph for all components
|
|
578
|
+
|
|
579
|
+
Args:
|
|
580
|
+
project_id: Project ID
|
|
581
|
+
base_path: Optional base path to scan
|
|
582
|
+
|
|
583
|
+
Returns:
|
|
584
|
+
Dict mapping component names to their dependencies
|
|
585
|
+
"""
|
|
586
|
+
discovered = self.discover_components(project_id, base_path)
|
|
587
|
+
|
|
588
|
+
graph = {}
|
|
589
|
+
for metadata in discovered:
|
|
590
|
+
deps = [d.component_name for d in metadata.dependencies]
|
|
591
|
+
graph[metadata.name] = deps
|
|
592
|
+
|
|
593
|
+
return graph
|
|
594
|
+
|
|
595
|
+
def find_unused_components(
|
|
596
|
+
self,
|
|
597
|
+
project_id: int,
|
|
598
|
+
base_path: Optional[str] = None
|
|
599
|
+
) -> List[str]:
|
|
600
|
+
"""Find components that are not imported by any other component"""
|
|
601
|
+
graph = self.get_dependency_graph(project_id, base_path)
|
|
602
|
+
|
|
603
|
+
# Get all component names
|
|
604
|
+
all_components = set(graph.keys())
|
|
605
|
+
|
|
606
|
+
# Get all referenced components
|
|
607
|
+
referenced = set()
|
|
608
|
+
for deps in graph.values():
|
|
609
|
+
referenced.update(deps)
|
|
610
|
+
|
|
611
|
+
# Find unused (not referenced by any other)
|
|
612
|
+
unused = all_components - referenced
|
|
613
|
+
|
|
614
|
+
return list(unused)
|
|
615
|
+
|
|
616
|
+
def discover_existing_tests(
|
|
617
|
+
self,
|
|
618
|
+
project_id: int,
|
|
619
|
+
tests_dir: Optional[str] = None
|
|
620
|
+
) -> Dict[str, List[Dict[str, Any]]]:
|
|
621
|
+
"""
|
|
622
|
+
Discover existing test files and match them to components
|
|
623
|
+
|
|
624
|
+
Args:
|
|
625
|
+
project_id: Project ID
|
|
626
|
+
tests_dir: Directory containing tests (defaults to project/tests)
|
|
627
|
+
|
|
628
|
+
Returns:
|
|
629
|
+
Dict mapping component names to list of discovered test info
|
|
630
|
+
"""
|
|
631
|
+
project = self.db.query(Project).filter(Project.id == project_id).first()
|
|
632
|
+
if not project:
|
|
633
|
+
return {}
|
|
634
|
+
|
|
635
|
+
# Determine tests directory
|
|
636
|
+
if not tests_dir:
|
|
637
|
+
base = project.source_path or "."
|
|
638
|
+
tests_dir = os.path.join(base, "tests")
|
|
639
|
+
|
|
640
|
+
if not os.path.exists(tests_dir):
|
|
641
|
+
return {}
|
|
642
|
+
|
|
643
|
+
discovered_tests: Dict[str, List[Dict[str, Any]]] = {}
|
|
644
|
+
|
|
645
|
+
# Scan for test files
|
|
646
|
+
test_path = Path(tests_dir)
|
|
647
|
+
for test_file in test_path.glob("**/test_*.py"):
|
|
648
|
+
# Parse test file to extract test functions
|
|
649
|
+
try:
|
|
650
|
+
content = test_file.read_text(encoding='utf-8')
|
|
651
|
+
file_path = str(test_file)
|
|
652
|
+
|
|
653
|
+
# Extract component name from file (test_<component>.py)
|
|
654
|
+
file_stem = test_file.stem # test_user_login
|
|
655
|
+
component_name = file_stem.replace("test_", "").replace("_", "")
|
|
656
|
+
|
|
657
|
+
# Find test functions
|
|
658
|
+
test_pattern = re.compile(r'def (test_\w+)\s*\(')
|
|
659
|
+
for match in test_pattern.finditer(content):
|
|
660
|
+
test_name = match.group(1)
|
|
661
|
+
|
|
662
|
+
# Determine test type from path or name
|
|
663
|
+
test_type = "unit"
|
|
664
|
+
if "integration" in str(test_file).lower():
|
|
665
|
+
test_type = "integration"
|
|
666
|
+
elif "api" in str(test_file).lower() or "endpoint" in test_name.lower():
|
|
667
|
+
test_type = "api"
|
|
668
|
+
elif "e2e" in str(test_file).lower():
|
|
669
|
+
test_type = "e2e"
|
|
670
|
+
|
|
671
|
+
if component_name not in discovered_tests:
|
|
672
|
+
discovered_tests[component_name] = []
|
|
673
|
+
|
|
674
|
+
discovered_tests[component_name].append({
|
|
675
|
+
"test_file": file_path,
|
|
676
|
+
"test_name": test_name,
|
|
677
|
+
"test_type": test_type
|
|
678
|
+
})
|
|
679
|
+
|
|
680
|
+
except Exception as e:
|
|
681
|
+
logger.warning(f"Error parsing test file {test_file}: {e}")
|
|
682
|
+
|
|
683
|
+
return discovered_tests
|
|
684
|
+
|
|
685
|
+
def sync_component_tests(
|
|
686
|
+
self,
|
|
687
|
+
project_id: int,
|
|
688
|
+
tests_dir: Optional[str] = None
|
|
689
|
+
) -> Tuple[int, int]:
|
|
690
|
+
"""
|
|
691
|
+
Sync discovered tests with database
|
|
692
|
+
|
|
693
|
+
Returns:
|
|
694
|
+
Tuple of (tests_synced, components_updated)
|
|
695
|
+
"""
|
|
696
|
+
from datetime import datetime
|
|
697
|
+
|
|
698
|
+
discovered = self.discover_existing_tests(project_id, tests_dir)
|
|
699
|
+
tests_synced = 0
|
|
700
|
+
components_updated = 0
|
|
701
|
+
|
|
702
|
+
# Get all components for this project
|
|
703
|
+
components = self.db.query(Component).filter(
|
|
704
|
+
Component.project_id == project_id
|
|
705
|
+
).all()
|
|
706
|
+
|
|
707
|
+
component_map = {c.name.lower(): c for c in components}
|
|
708
|
+
|
|
709
|
+
for comp_name_key, tests in discovered.items():
|
|
710
|
+
# Try to find matching component
|
|
711
|
+
component = component_map.get(comp_name_key.lower())
|
|
712
|
+
if not component:
|
|
713
|
+
# Try with underscores removed
|
|
714
|
+
for name, comp in component_map.items():
|
|
715
|
+
if name.replace("_", "") == comp_name_key.lower():
|
|
716
|
+
component = comp
|
|
717
|
+
break
|
|
718
|
+
|
|
719
|
+
if not component:
|
|
720
|
+
continue
|
|
721
|
+
|
|
722
|
+
# Get existing tests for this component
|
|
723
|
+
existing_tests = self.db.query(ComponentTest).filter(
|
|
724
|
+
ComponentTest.component_id == component.id
|
|
725
|
+
).all()
|
|
726
|
+
existing_map = {(t.test_file, t.test_name): t for t in existing_tests}
|
|
727
|
+
|
|
728
|
+
for test_info in tests:
|
|
729
|
+
key = (test_info["test_file"], test_info["test_name"])
|
|
730
|
+
if key not in existing_map:
|
|
731
|
+
# Create new test record
|
|
732
|
+
test = ComponentTest(
|
|
733
|
+
component_id=component.id,
|
|
734
|
+
test_file=test_info["test_file"],
|
|
735
|
+
test_name=test_info["test_name"],
|
|
736
|
+
test_type=test_info["test_type"],
|
|
737
|
+
generated_by="discovered",
|
|
738
|
+
last_status="pending"
|
|
739
|
+
)
|
|
740
|
+
self.db.add(test)
|
|
741
|
+
tests_synced += 1
|
|
742
|
+
|
|
743
|
+
# Update component test count
|
|
744
|
+
new_test_count = len(tests)
|
|
745
|
+
if component.test_count != new_test_count:
|
|
746
|
+
component.test_count = new_test_count
|
|
747
|
+
components_updated += 1
|
|
748
|
+
|
|
749
|
+
self.db.commit()
|
|
750
|
+
return tests_synced, components_updated
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
# Singleton instance
|
|
754
|
+
_discovery_service = None
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def get_discovery_service(db: Session = None) -> ComponentDiscoveryService:
|
|
758
|
+
"""Get component discovery service instance"""
|
|
759
|
+
if db:
|
|
760
|
+
return ComponentDiscoveryService(db)
|
|
761
|
+
|
|
762
|
+
global _discovery_service
|
|
763
|
+
if _discovery_service is None:
|
|
764
|
+
from .database import SessionLocal
|
|
765
|
+
_discovery_service = ComponentDiscoveryService(SessionLocal())
|
|
766
|
+
return _discovery_service
|