qtype 0.0.16__py3-none-any.whl → 0.1.1__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 (128) hide show
  1. qtype/application/commons/tools.py +1 -1
  2. qtype/application/converters/tools_from_api.py +5 -5
  3. qtype/application/converters/tools_from_module.py +2 -2
  4. qtype/application/converters/types.py +14 -43
  5. qtype/application/documentation.py +1 -1
  6. qtype/application/facade.py +94 -73
  7. qtype/base/types.py +227 -7
  8. qtype/cli.py +4 -0
  9. qtype/commands/convert.py +20 -8
  10. qtype/commands/generate.py +19 -27
  11. qtype/commands/run.py +73 -36
  12. qtype/commands/serve.py +74 -54
  13. qtype/commands/validate.py +34 -8
  14. qtype/commands/visualize.py +46 -22
  15. qtype/dsl/__init__.py +6 -5
  16. qtype/dsl/custom_types.py +1 -1
  17. qtype/dsl/domain_types.py +65 -5
  18. qtype/dsl/linker.py +384 -0
  19. qtype/dsl/loader.py +315 -0
  20. qtype/dsl/model.py +612 -363
  21. qtype/dsl/parser.py +200 -0
  22. qtype/dsl/types.py +50 -0
  23. qtype/interpreter/api.py +57 -136
  24. qtype/interpreter/auth/aws.py +19 -9
  25. qtype/interpreter/auth/generic.py +93 -16
  26. qtype/interpreter/base/base_step_executor.py +436 -0
  27. qtype/interpreter/base/batch_step_executor.py +171 -0
  28. qtype/interpreter/base/exceptions.py +50 -0
  29. qtype/interpreter/base/executor_context.py +74 -0
  30. qtype/interpreter/base/factory.py +117 -0
  31. qtype/interpreter/base/progress_tracker.py +110 -0
  32. qtype/interpreter/base/secrets.py +339 -0
  33. qtype/interpreter/base/step_cache.py +74 -0
  34. qtype/interpreter/base/stream_emitter.py +469 -0
  35. qtype/interpreter/conversions.py +462 -22
  36. qtype/interpreter/converters.py +77 -0
  37. qtype/interpreter/endpoints.py +355 -0
  38. qtype/interpreter/executors/agent_executor.py +242 -0
  39. qtype/interpreter/executors/aggregate_executor.py +93 -0
  40. qtype/interpreter/executors/decoder_executor.py +163 -0
  41. qtype/interpreter/executors/doc_to_text_executor.py +112 -0
  42. qtype/interpreter/executors/document_embedder_executor.py +107 -0
  43. qtype/interpreter/executors/document_search_executor.py +122 -0
  44. qtype/interpreter/executors/document_source_executor.py +118 -0
  45. qtype/interpreter/executors/document_splitter_executor.py +105 -0
  46. qtype/interpreter/executors/echo_executor.py +63 -0
  47. qtype/interpreter/executors/field_extractor_executor.py +160 -0
  48. qtype/interpreter/executors/file_source_executor.py +101 -0
  49. qtype/interpreter/executors/file_writer_executor.py +110 -0
  50. qtype/interpreter/executors/index_upsert_executor.py +228 -0
  51. qtype/interpreter/executors/invoke_embedding_executor.py +92 -0
  52. qtype/interpreter/executors/invoke_flow_executor.py +51 -0
  53. qtype/interpreter/executors/invoke_tool_executor.py +358 -0
  54. qtype/interpreter/executors/llm_inference_executor.py +272 -0
  55. qtype/interpreter/executors/prompt_template_executor.py +78 -0
  56. qtype/interpreter/executors/sql_source_executor.py +106 -0
  57. qtype/interpreter/executors/vector_search_executor.py +91 -0
  58. qtype/interpreter/flow.py +159 -22
  59. qtype/interpreter/metadata_api.py +115 -0
  60. qtype/interpreter/resource_cache.py +5 -4
  61. qtype/interpreter/rich_progress.py +225 -0
  62. qtype/interpreter/stream/chat/__init__.py +15 -0
  63. qtype/interpreter/stream/chat/converter.py +391 -0
  64. qtype/interpreter/{chat → stream/chat}/file_conversions.py +2 -2
  65. qtype/interpreter/stream/chat/ui_request_to_domain_type.py +140 -0
  66. qtype/interpreter/stream/chat/vercel.py +609 -0
  67. qtype/interpreter/stream/utils/__init__.py +15 -0
  68. qtype/interpreter/stream/utils/build_vercel_ai_formatter.py +74 -0
  69. qtype/interpreter/stream/utils/callback_to_stream.py +66 -0
  70. qtype/interpreter/stream/utils/create_streaming_response.py +18 -0
  71. qtype/interpreter/stream/utils/default_chat_extract_text.py +20 -0
  72. qtype/interpreter/stream/utils/error_streaming_response.py +20 -0
  73. qtype/interpreter/telemetry.py +135 -8
  74. qtype/interpreter/tools/__init__.py +5 -0
  75. qtype/interpreter/tools/function_tool_helper.py +265 -0
  76. qtype/interpreter/types.py +330 -0
  77. qtype/interpreter/typing.py +83 -89
  78. qtype/interpreter/ui/404/index.html +1 -1
  79. qtype/interpreter/ui/404.html +1 -1
  80. qtype/interpreter/ui/_next/static/{nUaw6_IwRwPqkzwe5s725 → 20HoJN6otZ_LyHLHpCPE6}/_buildManifest.js +1 -1
  81. qtype/interpreter/ui/_next/static/chunks/{393-8fd474427f8e19ce.js → 434-b2112d19f25c44ff.js} +3 -3
  82. qtype/interpreter/ui/_next/static/chunks/app/page-8c67d16ac90d23cb.js +1 -0
  83. qtype/interpreter/ui/_next/static/chunks/ba12c10f-546f2714ff8abc66.js +1 -0
  84. qtype/interpreter/ui/_next/static/css/8a8d1269e362fef7.css +3 -0
  85. qtype/interpreter/ui/icon.png +0 -0
  86. qtype/interpreter/ui/index.html +1 -1
  87. qtype/interpreter/ui/index.txt +4 -4
  88. qtype/semantic/checker.py +583 -0
  89. qtype/semantic/generate.py +262 -83
  90. qtype/semantic/loader.py +95 -0
  91. qtype/semantic/model.py +436 -159
  92. qtype/semantic/resolver.py +63 -19
  93. qtype/semantic/visualize.py +28 -31
  94. {qtype-0.0.16.dist-info → qtype-0.1.1.dist-info}/METADATA +16 -3
  95. qtype-0.1.1.dist-info/RECORD +135 -0
  96. qtype/dsl/base_types.py +0 -38
  97. qtype/dsl/validator.py +0 -465
  98. qtype/interpreter/batch/__init__.py +0 -0
  99. qtype/interpreter/batch/file_sink_source.py +0 -162
  100. qtype/interpreter/batch/flow.py +0 -95
  101. qtype/interpreter/batch/sql_source.py +0 -92
  102. qtype/interpreter/batch/step.py +0 -74
  103. qtype/interpreter/batch/types.py +0 -41
  104. qtype/interpreter/batch/utils.py +0 -178
  105. qtype/interpreter/chat/chat_api.py +0 -237
  106. qtype/interpreter/chat/vercel.py +0 -314
  107. qtype/interpreter/exceptions.py +0 -10
  108. qtype/interpreter/step.py +0 -67
  109. qtype/interpreter/steps/__init__.py +0 -0
  110. qtype/interpreter/steps/agent.py +0 -114
  111. qtype/interpreter/steps/condition.py +0 -36
  112. qtype/interpreter/steps/decoder.py +0 -88
  113. qtype/interpreter/steps/llm_inference.py +0 -171
  114. qtype/interpreter/steps/prompt_template.py +0 -54
  115. qtype/interpreter/steps/search.py +0 -24
  116. qtype/interpreter/steps/tool.py +0 -219
  117. qtype/interpreter/streaming_helpers.py +0 -123
  118. qtype/interpreter/ui/_next/static/chunks/app/page-7e26b6156cfb55d3.js +0 -1
  119. qtype/interpreter/ui/_next/static/chunks/ba12c10f-22556063851a6df2.js +0 -1
  120. qtype/interpreter/ui/_next/static/css/b40532b0db09cce3.css +0 -3
  121. qtype/interpreter/ui/favicon.ico +0 -0
  122. qtype/loader.py +0 -390
  123. qtype-0.0.16.dist-info/RECORD +0 -106
  124. /qtype/interpreter/ui/_next/static/{nUaw6_IwRwPqkzwe5s725 → 20HoJN6otZ_LyHLHpCPE6}/_ssgManifest.js +0 -0
  125. {qtype-0.0.16.dist-info → qtype-0.1.1.dist-info}/WHEEL +0 -0
  126. {qtype-0.0.16.dist-info → qtype-0.1.1.dist-info}/entry_points.txt +0 -0
  127. {qtype-0.0.16.dist-info → qtype-0.1.1.dist-info}/licenses/LICENSE +0 -0
  128. {qtype-0.0.16.dist-info → qtype-0.1.1.dist-info}/top_level.txt +0 -0
@@ -92,6 +92,17 @@ def generate_schema(args: argparse.Namespace) -> None:
92
92
  if "$defs" not in schema:
93
93
  schema["$defs"] = {}
94
94
 
95
+ # Note: Custom YAML tags (!include, !include_raw) and environment variable
96
+ # substitution (${VAR}) are handled by the QType YAML loader at parse time,
97
+ # not by JSON Schema validation. We define them in $defs for documentation
98
+ # purposes, but we don't apply them to string fields since:
99
+ # 1. They would cause false positives (e.g., "localhost" matching as valid)
100
+ # 2. The YAML loader processes these before schema validation occurs
101
+ # 3. After YAML loading, the schema sees the resolved/substituted values
102
+ #
103
+ # Schema validation happens on the post-processed document structure,
104
+ # so we don't need to (and shouldn't) validate the raw YAML tag syntax.
105
+
95
106
  # Define custom YAML tags used by QType loader
96
107
  schema["$defs"]["qtype_include_tag"] = {
97
108
  "type": "string",
@@ -111,29 +122,6 @@ def generate_schema(args: argparse.Namespace) -> None:
111
122
  "description": "String with environment variable substitution using ${VAR_NAME} or ${VAR_NAME:default} syntax",
112
123
  }
113
124
 
114
- # Add these custom patterns to string types throughout the schema
115
- def add_custom_patterns(obj):
116
- if isinstance(obj, dict):
117
- if obj.get("type") == "string" and "anyOf" not in obj:
118
- # Add anyOf to allow either regular strings or custom tag patterns
119
- original_obj = obj.copy()
120
- obj.clear()
121
- obj["anyOf"] = [
122
- original_obj,
123
- {"$ref": "#/$defs/qtype_include_tag"},
124
- {"$ref": "#/$defs/qtype_include_raw_tag"},
125
- {"$ref": "#/$defs/qtype_env_var"},
126
- ]
127
- else:
128
- for value in obj.values():
129
- add_custom_patterns(value)
130
- elif isinstance(obj, list):
131
- for item in obj:
132
- add_custom_patterns(item)
133
-
134
- # Apply custom patterns to the schema
135
- add_custom_patterns(schema)
136
-
137
125
  output = json.dumps(schema, indent=2)
138
126
  output_path: Optional[str] = getattr(args, "output", None)
139
127
  if output_path:
@@ -198,6 +186,14 @@ def parser(subparsers: argparse._SubParsersAction) -> None:
198
186
  import networkx # noqa: F401
199
187
  import ruff # type: ignore[import-untyped] # noqa: F401
200
188
 
189
+ has_semantic_deps = True
190
+ except ImportError:
191
+ logger.warning(
192
+ "NetworkX or Ruff is not installed. Skipping semantic model generation."
193
+ )
194
+ has_semantic_deps = False
195
+
196
+ if has_semantic_deps:
201
197
  from qtype.semantic.generate import generate_semantic_model
202
198
 
203
199
  semantic_parser = generate_subparsers.add_parser(
@@ -212,7 +208,3 @@ def parser(subparsers: argparse._SubParsersAction) -> None:
212
208
  help="Output file for the semantic model (default: stdout)",
213
209
  )
214
210
  semantic_parser.set_defaults(func=generate_semantic_model)
215
- except ImportError:
216
- logger.debug(
217
- "NetworkX or Ruff is not installed. Skipping semantic model generation."
218
- )
qtype/commands/run.py CHANGED
@@ -7,11 +7,12 @@ from __future__ import annotations
7
7
  import argparse
8
8
  import json
9
9
  import logging
10
+ import warnings
10
11
  from pathlib import Path
11
12
  from typing import Any
12
13
 
13
- import magic
14
14
  import pandas as pd
15
+ from pydantic.warnings import UnsupportedFieldAttributeWarning
15
16
 
16
17
  from qtype.application.facade import QTypeFacade
17
18
  from qtype.base.exceptions import InterpreterError, LoadError, ValidationError
@@ -19,14 +20,41 @@ from qtype.base.exceptions import InterpreterError, LoadError, ValidationError
19
20
  logger = logging.getLogger(__name__)
20
21
 
21
22
 
23
+ # Supress specific pydantic warnings that llamaindex needs to fix
24
+ warnings.filterwarnings("ignore", category=UnsupportedFieldAttributeWarning)
25
+
26
+
27
+ # supress qdrant logging
28
+ for name in ["httpx", "urllib3", "qdrant_client"]:
29
+ logging.getLogger(name).setLevel(logging.WARNING)
30
+
31
+
22
32
  def read_data_from_file(file_path: str) -> pd.DataFrame:
23
33
  """
24
34
  Reads a file into a pandas DataFrame based on its MIME type.
25
35
  """
36
+ from pathlib import Path
37
+
38
+ import magic
39
+
26
40
  mime_type = magic.Magic(mime=True).from_file(file_path)
27
41
 
28
42
  if mime_type == "text/csv":
29
43
  return pd.read_csv(file_path)
44
+ elif mime_type == "text/plain":
45
+ # For text/plain, use file extension to determine format
46
+ file_ext = Path(file_path).suffix.lower()
47
+ if file_ext == ".csv":
48
+ return pd.read_csv(file_path)
49
+ elif file_ext == ".json":
50
+ return pd.read_json(file_path)
51
+ else:
52
+ raise ValueError(
53
+ (
54
+ f"Unsupported text/plain file extension: {file_ext}. "
55
+ "Supported extensions: .csv, .json"
56
+ )
57
+ )
30
58
  elif mime_type == "application/json":
31
59
  return pd.read_json(file_path)
32
60
  elif mime_type in [
@@ -48,6 +76,8 @@ def run_flow(args: Any) -> None:
48
76
  Args:
49
77
  args: Arguments passed from the command line or calling context.
50
78
  """
79
+ import asyncio
80
+
51
81
  facade = QTypeFacade()
52
82
  spec_path = Path(args.spec)
53
83
 
@@ -65,54 +95,56 @@ def run_flow(args: Any) -> None:
65
95
  logger.error(f"❌ Invalid JSON input: {e}")
66
96
  return
67
97
 
68
- # Execute the workflow using the facade
69
- result = facade.execute_workflow(
70
- spec_path, flow_name=args.flow, inputs=input, batch_config=None
98
+ # Execute the workflow using the facade (now async, returns DataFrame)
99
+ result_df = asyncio.run(
100
+ facade.execute_workflow(
101
+ spec_path,
102
+ flow_name=args.flow,
103
+ inputs=input,
104
+ show_progress=args.progress,
105
+ )
71
106
  )
72
107
 
73
108
  logger.info("✅ Flow execution completed successfully")
74
109
 
75
- # Print results
76
- if isinstance(result, pd.DataFrame):
77
- logging.info("Output DataFrame:")
78
- logging.info(result)
79
- elif (
80
- result
81
- and hasattr(result, "__iter__")
82
- and not isinstance(result, str)
83
- ):
84
- # If result is a list of variables or similar
85
- try:
86
- for item in result:
87
- if hasattr(item, "id") and hasattr(item, "value"):
88
- logger.info(f"Output {item.id}: {item.value}")
89
- else:
90
- logger.info(f"Result: {item}")
91
- except TypeError:
92
- logger.info(f"Result: {result}")
93
- elif isinstance(result, str):
94
- logger.info(f"Result: {result}")
110
+ # Display results
111
+ if len(result_df) > 0:
112
+ logger.info(f"Processed {len(result_df)} em")
113
+
114
+ # Remove 'row' and 'error' columns for display if all errors are None
115
+ display_df = result_df.copy()
116
+ if (
117
+ "error" in display_df.columns
118
+ and display_df["error"].isna().all()
119
+ ):
120
+ display_df = display_df.drop(columns=["error"])
121
+ if "row" in display_df.columns:
122
+ display_df = display_df.drop(columns=["row"])
123
+
124
+ if len(display_df) > 1:
125
+ logger.info(f"\nResults:\n{display_df[0:10].to_string()}\n...")
126
+ else:
127
+ # Print the first row with column_name: value one per line
128
+ fmt_str = []
129
+ for col, val in display_df.iloc[0].items():
130
+ fmt_str.append(f"{col}: {val}")
131
+ fmt_str = "\n".join(fmt_str)
132
+ logger.info(f"\nResults:\n{fmt_str}")
133
+
134
+ # Save the output
135
+ if args.output:
136
+ # Save full DataFrame with row and error columns
137
+ result_df.to_parquet(args.output)
138
+ logger.info(f"Output saved to {args.output}")
95
139
  else:
96
140
  logger.info("Flow completed with no output")
97
141
 
98
- # save the output
99
- if isinstance(result, pd.DataFrame) and args.output:
100
- result.to_parquet(args.output)
101
- logger.info(f"Output DataFrame saved to {args.output}")
102
- elif args.output:
103
- with open(args.output, "w") as f:
104
- json.dump(result, f, indent=2)
105
- logger.info(f"Output saved to {args.output}")
106
-
107
142
  except LoadError as e:
108
143
  logger.error(f"❌ Failed to load document: {e}")
109
144
  except ValidationError as e:
110
145
  logger.error(f"❌ Validation failed: {e}")
111
146
  except InterpreterError as e:
112
147
  logger.error(f"❌ Execution failed: {e}")
113
- except Exception as e:
114
- logger.error(f"❌ Unexpected error: {e}", exc_info=True)
115
- pass
116
148
 
117
149
 
118
150
  def parser(subparsers: argparse._SubParsersAction) -> None:
@@ -154,6 +186,11 @@ def parser(subparsers: argparse._SubParsersAction) -> None:
154
186
  default=None,
155
187
  help="Path to save output data. If input is a DataFrame, output will be saved as parquet. If single result, saved as JSON.",
156
188
  )
189
+ cmd_parser.add_argument(
190
+ "--progress",
191
+ action="store_true",
192
+ help="Show progress bars during flow execution.",
193
+ )
157
194
 
158
195
  cmd_parser.add_argument(
159
196
  "spec", type=str, help="Path to the QType YAML spec file."
qtype/commands/serve.py CHANGED
@@ -6,71 +6,91 @@ from __future__ import annotations
6
6
 
7
7
  import argparse
8
8
  import logging
9
+ import os
9
10
  from pathlib import Path
10
11
  from typing import Any
11
12
 
12
13
  import uvicorn
13
14
 
14
- from qtype.application.facade import QTypeFacade
15
- from qtype.base.exceptions import LoadError, ValidationError
15
+ from qtype.base.exceptions import ValidationError
16
+ from qtype.semantic.loader import load
17
+ from qtype.semantic.model import Application
16
18
 
17
19
  logger = logging.getLogger(__name__)
18
20
 
19
21
 
20
- def serve(args: Any) -> None:
22
+ def create_api_app() -> Any:
23
+ """Factory function to create FastAPI app.
24
+
25
+ Returns:
26
+ FastAPI application instance.
27
+
28
+ Raises:
29
+ RuntimeError: If QTYPE_SPEC_PATH not set in environment.
30
+ ValidationError: If spec is not an Application document.
31
+ """
32
+ from qtype.interpreter.api import APIExecutor
33
+
34
+ spec_path_str = os.environ["_QTYPE_SPEC_PATH"]
35
+
36
+ spec_path = Path(spec_path_str)
37
+ logger.info(f"Loading spec: {spec_path}")
38
+
39
+ semantic_model, _ = load(spec_path)
40
+ if not isinstance(semantic_model, Application):
41
+ raise ValidationError("Can only serve Application documents")
42
+
43
+ logger.info(f"✅ Successfully loaded spec: {spec_path}")
44
+
45
+ # Derive name from spec filename
46
+ name = spec_path.name.replace(".qtype.yaml", "").replace("_", " ").title()
47
+
48
+ # Get host/port from environment (set by uvicorn)
49
+ host = os.environ["_QTYPE_HOST"]
50
+ port = int(os.environ["_QTYPE_PORT"])
51
+
52
+ # Create server info for OpenAPI spec
53
+ servers = [
54
+ {
55
+ "url": f"http://{host}:{port}",
56
+ "description": "Development server",
57
+ }
58
+ ]
59
+
60
+ api_executor = APIExecutor(semantic_model, host, port)
61
+ return api_executor.create_app(
62
+ name=name,
63
+ ui_enabled=True,
64
+ servers=servers,
65
+ )
66
+
67
+
68
+ def serve(args: argparse.Namespace) -> None:
21
69
  """Run a QType YAML spec file as an API.
22
70
 
23
71
  Args:
24
- args: Arguments passed from the command line or calling context.
72
+ args: Arguments passed from the command line.
25
73
  """
26
- facade = QTypeFacade()
27
-
28
- try:
29
- # Use facade to load and validate the document
30
- spec_path = Path(args.spec)
31
- logger.info(f"Loading and validating spec: {spec_path}")
32
-
33
- semantic_model, type_registry = facade.load_semantic_model(spec_path)
34
- facade.telemetry(semantic_model)
35
- logger.info(f"✅ Successfully loaded spec: {spec_path}")
36
-
37
- # Import APIExecutor and create the FastAPI app
38
- from qtype.interpreter.api import APIExecutor
39
-
40
- # Get the name from the spec filename
41
- name = (
42
- spec_path.name.replace(".qtype.yaml", "").replace("_", " ").title()
43
- )
44
-
45
- logger.info(f"Starting server for: {name}")
46
- api_executor = APIExecutor(semantic_model)
47
-
48
- # Create server info for OpenAPI spec
49
- servers = [
50
- {
51
- "url": f"http://{args.host}:{args.port}",
52
- "description": "Development server",
53
- }
54
- ]
55
-
56
- fastapi_app = api_executor.create_app(
57
- name=name, ui_enabled=not args.disable_ui, servers=servers
58
- )
59
-
60
- # Start the server
61
- uvicorn.run(
62
- fastapi_app, host=args.host, port=args.port, log_level="info"
63
- )
64
-
65
- except LoadError as e:
66
- logger.error(f"❌ Failed to load document: {e}")
67
- exit(1)
68
- except ValidationError as e:
69
- logger.error(f"❌ Validation failed: {e}")
70
- exit(1)
71
- except Exception as e:
72
- logger.error(f"❌ Unexpected error starting server: {e}")
73
- exit(1)
74
+ # Set environment variables for factory function
75
+ os.environ["_QTYPE_SPEC_PATH"] = args.spec
76
+ os.environ["_QTYPE_HOST"] = args.host
77
+ os.environ["_QTYPE_PORT"] = str(args.port)
78
+
79
+ logger.info(
80
+ f"Starting server on {args.host}:{args.port}"
81
+ f"{' (reload enabled)' if args.reload else ''}"
82
+ )
83
+
84
+ # Use factory mode with import string
85
+ uvicorn.run(
86
+ "qtype.commands.serve:create_api_app",
87
+ factory=True,
88
+ host=args.host,
89
+ port=args.port,
90
+ log_level="info",
91
+ reload=args.reload,
92
+ reload_includes=[args.spec] if args.reload else None,
93
+ )
74
94
 
75
95
 
76
96
  def parser(subparsers: argparse._SubParsersAction) -> None:
@@ -86,9 +106,9 @@ def parser(subparsers: argparse._SubParsersAction) -> None:
86
106
  cmd_parser.add_argument("-p", "--port", type=int, default=8000)
87
107
  cmd_parser.add_argument("-H", "--host", type=str, default="localhost")
88
108
  cmd_parser.add_argument(
89
- "--disable-ui",
109
+ "--reload",
90
110
  action="store_true",
91
- help="Disable the UI for the QType application.",
111
+ help="Enable auto-reload on code changes (default: False).",
92
112
  )
93
113
  cmd_parser.set_defaults(func=serve)
94
114
 
@@ -10,9 +10,12 @@ import sys
10
10
  from pathlib import Path
11
11
  from typing import Any
12
12
 
13
- from qtype import dsl
14
- from qtype.application.facade import QTypeFacade
15
13
  from qtype.base.exceptions import LoadError, SemanticError, ValidationError
14
+ from qtype.dsl.linker import DuplicateComponentError, ReferenceNotFoundError
15
+ from qtype.dsl.loader import YAMLLoadError
16
+ from qtype.dsl.model import Application as DSLApplication
17
+ from qtype.dsl.model import Document
18
+ from qtype.semantic.loader import load
16
19
 
17
20
  logger = logging.getLogger(__name__)
18
21
 
@@ -27,19 +30,33 @@ def main(args: Any) -> None:
27
30
  Exits:
28
31
  Exits with code 1 if validation fails.
29
32
  """
30
- facade = QTypeFacade()
31
33
  spec_path = Path(args.spec)
32
34
 
33
35
  try:
34
- # Use the facade for validation - it will raise exceptions on errors
35
- loaded_data, custom_types = facade.load_dsl_document(spec_path)
36
- if isinstance(loaded_data, dsl.Application):
37
- loaded_data, custom_types = facade.load_semantic_model(spec_path)
36
+ # Load and validate the document (works for all document types)
37
+ # This includes: YAML parsing, Pydantic validation, linking, and semantic checks
38
+ loaded_data, custom_types = load(spec_path)
38
39
  logger.info("✅ Validation successful - document is valid.")
39
40
 
40
41
  except LoadError as e:
41
42
  logger.error(f"❌ Failed to load document: {e}")
42
43
  sys.exit(1)
44
+ except YAMLLoadError as e:
45
+ # YAML syntax errors
46
+ logger.error(f"❌ {e}")
47
+ sys.exit(1)
48
+ except DuplicateComponentError as e:
49
+ # Duplicate ID errors during linking
50
+ logger.error(f"❌ {e}")
51
+ sys.exit(1)
52
+ except ReferenceNotFoundError as e:
53
+ # Reference resolution errors during linking
54
+ logger.error(f"❌ {e}")
55
+ sys.exit(1)
56
+ except ValueError as e:
57
+ # Pydantic validation errors from parse_document
58
+ logger.error(f"❌ {e}")
59
+ sys.exit(1)
43
60
  except ValidationError as e:
44
61
  logger.error(f"❌ Validation failed: {e}")
45
62
  sys.exit(1)
@@ -49,7 +66,16 @@ def main(args: Any) -> None:
49
66
 
50
67
  # If printing is requested, load and print the document
51
68
  if args.print:
52
- logging.info(facade.convert_document(loaded_data)) # type: ignore
69
+ from pydantic_yaml import to_yaml_str
70
+
71
+ # Wrap in Document if it's a DSL Application
72
+ if isinstance(loaded_data, DSLApplication):
73
+ wrapped = Document(root=loaded_data)
74
+ else:
75
+ wrapped = loaded_data
76
+ logging.info(
77
+ to_yaml_str(wrapped, exclude_unset=True, exclude_none=True)
78
+ )
53
79
 
54
80
 
55
81
  def parser(subparsers: argparse._SubParsersAction) -> None:
@@ -6,13 +6,17 @@ from __future__ import annotations
6
6
 
7
7
  import argparse
8
8
  import logging
9
+ import shutil
10
+ import subprocess
9
11
  import tempfile
10
12
  import webbrowser
11
13
  from pathlib import Path
12
14
  from typing import Any
13
15
 
14
- from qtype.application.facade import QTypeFacade
15
16
  from qtype.base.exceptions import LoadError, ValidationError
17
+ from qtype.semantic.loader import load
18
+ from qtype.semantic.model import Application
19
+ from qtype.semantic.visualize import visualize_application
16
20
 
17
21
  logger = logging.getLogger(__name__)
18
22
 
@@ -27,12 +31,13 @@ def main(args: Any) -> None:
27
31
  Exits:
28
32
  Exits with code 1 if visualization fails.
29
33
  """
30
- facade = QTypeFacade()
31
34
  spec_path = Path(args.spec)
32
35
 
33
36
  try:
34
- # Generate visualization using the facade
35
- mermaid_content = facade.visualize_application(spec_path)
37
+ # Load and generate visualization
38
+ semantic_model, _ = load(spec_path)
39
+ assert isinstance(semantic_model, Application)
40
+ mermaid_content = visualize_application(semantic_model)
36
41
 
37
42
  if args.output:
38
43
  # Write to file
@@ -41,26 +46,45 @@ def main(args: Any) -> None:
41
46
  logger.info(f"✅ Visualization saved to {output_path}")
42
47
 
43
48
  if not args.no_display:
44
- # Create temporary HTML file and open in browser
49
+ # Check if mmdc is available
50
+ if shutil.which("mmdc") is None:
51
+ logger.error(
52
+ "❌ mmdc command not found. Please install mermaid-cli."
53
+ )
54
+ logger.info(
55
+ "Install with: npm install -g @mermaid-js/mermaid-cli"
56
+ )
57
+ exit(1)
58
+
59
+ # Create temporary directory and run mmdc from within it
45
60
  try:
46
- import mermaid as md # type: ignore[import-untyped]
47
-
48
- mm = md.Mermaid(mermaid_content)
49
- html_content = mm._repr_html_()
50
-
51
- with tempfile.NamedTemporaryFile(
52
- mode="w", suffix=".html", delete=False, encoding="utf-8"
53
- ) as f:
54
- f.write(html_content)
55
- temp_file = f.name
56
-
57
- logger.info(f"Opening visualization in browser: {temp_file}")
58
- webbrowser.open(f"file://{temp_file}")
59
- except ImportError:
60
- logger.warning(
61
- "❌ Mermaid library not installed. Cannot display in browser."
61
+ # Create a temporary directory
62
+ temp_dir = tempfile.mkdtemp()
63
+ temp_dir_path = Path(temp_dir)
64
+
65
+ # Write mermaid file with simple names in the temp directory
66
+ mmd_file_path = temp_dir_path / "diagram.mmd"
67
+ svg_file_path = temp_dir_path / "diagram.svg"
68
+
69
+ mmd_file_path.write_text(mermaid_content, encoding="utf-8")
70
+
71
+ # Run mmdc from within the temporary directory
72
+ subprocess.run(
73
+ ["mmdc", "-i", "diagram.mmd", "-o", "diagram.svg"],
74
+ cwd=temp_dir,
75
+ capture_output=True,
76
+ text=True,
77
+ check=True,
62
78
  )
63
- logger.info("Install with: pip install mermaid")
79
+
80
+ logger.info(
81
+ f"Opening visualization in browser: {svg_file_path}"
82
+ )
83
+ webbrowser.open(f"file://{svg_file_path}")
84
+
85
+ except subprocess.CalledProcessError as e:
86
+ logger.error(f"❌ Failed to generate SVG: {e.stderr}")
87
+ exit(1)
64
88
 
65
89
  except LoadError as e:
66
90
  logger.error(f"❌ Failed to load document: {e}")
qtype/dsl/__init__.py CHANGED
@@ -2,9 +2,10 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from .base_types import * # noqa: F403
6
- from .domain_types import * # noqa: F403
7
- from .model import * # noqa: F403
5
+ # Import all public symbols from submodules
6
+ # The star import is intentional here - it's controlled by __all__ in each module
7
+ from qtype.base.types import Reference # noqa: F401
8
8
 
9
- # Note: Validation logic has been moved to qtype.semantic package
10
- # to avoid circular dependencies
9
+ from .domain_types import * # noqa: F403, F401
10
+ from .loader import YAMLLoadError # noqa: F401
11
+ from .model import * # noqa: F403, F401
qtype/dsl/custom_types.py CHANGED
@@ -2,7 +2,7 @@ from typing import Any, ForwardRef, Type, Union
2
2
 
3
3
  from pydantic import BaseModel, create_model
4
4
 
5
- from qtype.application.converters.types import PRIMITIVE_TO_PYTHON_TYPE
5
+ from qtype.dsl.types import PRIMITIVE_TO_PYTHON_TYPE
6
6
 
7
7
  # --- This would be in your interpreter's logic ---
8
8