doql 0.0.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.
- doql/__init__.py +7 -0
- doql/cli/__init__.py +19 -0
- doql/cli/build.py +159 -0
- doql/cli/commands/__init__.py +38 -0
- doql/cli/commands/deploy.py +25 -0
- doql/cli/commands/docs.py +21 -0
- doql/cli/commands/export.py +36 -0
- doql/cli/commands/generate.py +34 -0
- doql/cli/commands/init.py +43 -0
- doql/cli/commands/kiosk.py +20 -0
- doql/cli/commands/plan.py +115 -0
- doql/cli/commands/quadlet.py +22 -0
- doql/cli/commands/query.py +31 -0
- doql/cli/commands/render.py +26 -0
- doql/cli/commands/run.py +25 -0
- doql/cli/commands/validate.py +43 -0
- doql/cli/context.py +60 -0
- doql/cli/lockfile.py +88 -0
- doql/cli/main.py +117 -0
- doql/cli/sync.py +210 -0
- doql/cli.py +62 -0
- doql/generators/__init__.py +5 -0
- doql/generators/api_gen/__init__.py +167 -0
- doql/generators/api_gen/alembic.py +154 -0
- doql/generators/api_gen/auth.py +141 -0
- doql/generators/api_gen/common.py +79 -0
- doql/generators/api_gen/database.py +35 -0
- doql/generators/api_gen/main.py +74 -0
- doql/generators/api_gen/models.py +82 -0
- doql/generators/api_gen/routes.py +115 -0
- doql/generators/api_gen/schemas.py +83 -0
- doql/generators/api_gen.py +25 -0
- doql/generators/ci_gen.py +112 -0
- doql/generators/deploy.py +20 -0
- doql/generators/desktop_gen.py +209 -0
- doql/generators/docs_gen.py +24 -0
- doql/generators/document_gen.py +182 -0
- doql/generators/export_postman.py +25 -0
- doql/generators/export_ts_sdk.py +26 -0
- doql/generators/i18n_gen.py +168 -0
- doql/generators/infra_gen.py +321 -0
- doql/generators/integrations_gen.py +320 -0
- doql/generators/mobile_gen.py +462 -0
- doql/generators/report_gen.py +142 -0
- doql/generators/web_gen/__init__.py +176 -0
- doql/generators/web_gen/common.py +9 -0
- doql/generators/web_gen/components.py +76 -0
- doql/generators/web_gen/config.py +122 -0
- doql/generators/web_gen/core.py +57 -0
- doql/generators/web_gen/pages.py +163 -0
- doql/generators/web_gen/pwa.py +109 -0
- doql/generators/web_gen/router.py +43 -0
- doql/generators/web_gen.py +34 -0
- doql/generators/workflow_gen.py +304 -0
- doql/lsp_server.py +364 -0
- doql/parser.py +70 -0
- doql/parsers/__init__.py +117 -0
- doql/parsers/blocks.py +50 -0
- doql/parsers/extractors.py +191 -0
- doql/parsers/models.py +207 -0
- doql/parsers/registry.py +267 -0
- doql/parsers/validators.py +138 -0
- doql/plugins.py +101 -0
- doql/scaffolds/calibration-lab/.env.example +19 -0
- doql/scaffolds/calibration-lab/app.doql +135 -0
- doql/scaffolds/iot-fleet/.env.example +21 -0
- doql/scaffolds/iot-fleet/app.doql +99 -0
- doql/scaffolds/minimal/.env.example +8 -0
- doql/scaffolds/minimal/app.doql +35 -0
- doql/scaffolds/saas-multi-tenant/.env.example +22 -0
- doql/scaffolds/saas-multi-tenant/app.doql +97 -0
- doql/utils/__init__.py +6 -0
- doql/utils/naming.py +37 -0
- doql-0.0.1.dist-info/METADATA +252 -0
- doql-0.0.1.dist-info/RECORD +79 -0
- doql-0.0.1.dist-info/WHEEL +5 -0
- doql-0.0.1.dist-info/entry_points.txt +3 -0
- doql-0.0.1.dist-info/licenses/LICENSE +26 -0
- doql-0.0.1.dist-info/top_level.txt +1 -0
doql/__init__.py
ADDED
doql/cli/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""doql CLI package â modularized command-line interface."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from .context import BuildContext, build_context, load_spec, scaffold_from_template, estimate_file_count
|
|
5
|
+
from .lockfile import read_lockfile, write_lockfile, diff_sections, spec_section_hashes
|
|
6
|
+
from .main import main
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"BuildContext",
|
|
10
|
+
"build_context",
|
|
11
|
+
"load_spec",
|
|
12
|
+
"scaffold_from_template",
|
|
13
|
+
"estimate_file_count",
|
|
14
|
+
"read_lockfile",
|
|
15
|
+
"write_lockfile",
|
|
16
|
+
"diff_sections",
|
|
17
|
+
"spec_section_hashes",
|
|
18
|
+
"main",
|
|
19
|
+
]
|
doql/cli/build.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Full build logic for the build command.
|
|
2
|
+
|
|
3
|
+
This module handles complete rebuilds by running all applicable generators.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
import argparse
|
|
9
|
+
|
|
10
|
+
from .. import parser as doql_parser
|
|
11
|
+
from ..generators import api_gen, web_gen, mobile_gen, desktop_gen, infra_gen, document_gen, report_gen, i18n_gen, integrations_gen, workflow_gen, ci_gen
|
|
12
|
+
from .. import plugins as _plugins
|
|
13
|
+
from .context import BuildContext, load_spec
|
|
14
|
+
from .lockfile import write_lockfile
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def should_generate_interface(name: str, spec) -> bool:
|
|
18
|
+
"""Check if interface should be generated.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
name: Interface name (api, web, mobile, desktop, infra)
|
|
22
|
+
spec: Parsed DoqlSpec
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
True if interface should be generated
|
|
26
|
+
"""
|
|
27
|
+
if name == "infra":
|
|
28
|
+
return True
|
|
29
|
+
return any(i.name == name for i in spec.interfaces)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def run_core_generators(spec, env_vars, ctx: BuildContext) -> None:
|
|
33
|
+
"""Run core interface generators (api, web, mobile, desktop, infra)."""
|
|
34
|
+
generators = {
|
|
35
|
+
"api": api_gen.generate,
|
|
36
|
+
"web": web_gen.generate,
|
|
37
|
+
"mobile": mobile_gen.generate,
|
|
38
|
+
"desktop": desktop_gen.generate,
|
|
39
|
+
"infra": infra_gen.generate,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
for name, fn in generators.items():
|
|
43
|
+
if not should_generate_interface(name, spec):
|
|
44
|
+
continue
|
|
45
|
+
|
|
46
|
+
print(f"đ Generating {name}...")
|
|
47
|
+
if name == "infra":
|
|
48
|
+
target_dir = ctx.build_dir / name
|
|
49
|
+
else:
|
|
50
|
+
target_dir = ctx.build_dir / name
|
|
51
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
fn(spec, env_vars, target_dir)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def run_document_generators(spec, env_vars, ctx: BuildContext) -> None:
|
|
56
|
+
"""Run document generators if documents are defined."""
|
|
57
|
+
if not spec.documents:
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
print("đ Generating documents...")
|
|
61
|
+
doc_dir = ctx.build_dir / "documents"
|
|
62
|
+
doc_dir.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
document_gen.generate(spec, env_vars, doc_dir, project_root=ctx.root)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def run_report_generators(spec, env_vars, ctx: BuildContext) -> None:
|
|
67
|
+
"""Run report generators if reports are defined."""
|
|
68
|
+
if not spec.reports:
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
print("đ Generating reports...")
|
|
72
|
+
rpt_dir = ctx.build_dir / "reports"
|
|
73
|
+
rpt_dir.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
report_gen.generate(spec, env_vars, rpt_dir)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def run_i18n_generators(spec, env_vars, ctx: BuildContext) -> None:
|
|
78
|
+
"""Run i18n generators if languages are defined."""
|
|
79
|
+
if not spec.languages:
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
print("đ Generating i18n...")
|
|
83
|
+
i18n_dir = ctx.build_dir / "i18n"
|
|
84
|
+
i18n_dir.mkdir(parents=True, exist_ok=True)
|
|
85
|
+
i18n_gen.generate(spec, env_vars, i18n_dir)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def run_integration_generators(spec, env_vars, ctx: BuildContext) -> None:
|
|
89
|
+
"""Run integration generators if integrations are defined."""
|
|
90
|
+
if not (spec.integrations or spec.api_clients or spec.webhooks):
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
print("đ Generating integrations...")
|
|
94
|
+
svc_dir = ctx.build_dir / "api" / "services"
|
|
95
|
+
svc_dir.mkdir(parents=True, exist_ok=True)
|
|
96
|
+
integrations_gen.generate(spec, env_vars, svc_dir)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def run_workflow_generators(spec, env_vars, ctx: BuildContext) -> None:
|
|
100
|
+
"""Run workflow generators if workflows are defined."""
|
|
101
|
+
if not spec.workflows:
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
print("đ Generating workflows...")
|
|
105
|
+
wf_dir = ctx.build_dir / "api" / "workflows"
|
|
106
|
+
wf_dir.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
workflow_gen.generate(spec, env_vars, wf_dir)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def run_ci_generator(spec, env_vars, ctx: BuildContext) -> None:
|
|
111
|
+
"""Run CI/CD generator (always into project root)."""
|
|
112
|
+
print("đ Generating CI...")
|
|
113
|
+
ci_gen.generate(spec, env_vars, ctx.root)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def run_plugins(spec, env_vars, ctx: BuildContext) -> None:
|
|
117
|
+
"""Run plugin generators."""
|
|
118
|
+
_plugins.run_plugins(spec, env_vars, ctx.build_dir, ctx.root)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def cmd_build(args: argparse.Namespace) -> int:
|
|
122
|
+
"""Generate all code for the project.
|
|
123
|
+
|
|
124
|
+
This command runs all applicable generators to create a complete build.
|
|
125
|
+
Validation is performed first unless --force is specified.
|
|
126
|
+
"""
|
|
127
|
+
ctx = BuildContext(
|
|
128
|
+
root=__import__('pathlib').Path(getattr(args, "dir", None) or ".").resolve(),
|
|
129
|
+
doql_file=__import__('pathlib').Path(getattr(args, "dir", None) or ".").resolve() / (getattr(args, "file", None) or "app.doql"),
|
|
130
|
+
env_file=__import__('pathlib').Path(getattr(args, "dir", None) or ".").resolve() / ".env",
|
|
131
|
+
build_dir=__import__('pathlib').Path(getattr(args, "dir", None) or ".").resolve() / "build",
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
spec, env_vars = load_spec(ctx)
|
|
135
|
+
|
|
136
|
+
# Validate unless --force
|
|
137
|
+
issues = doql_parser.validate(spec, env_vars)
|
|
138
|
+
errors = [i for i in issues if i.severity == "error"]
|
|
139
|
+
if errors and not getattr(args, "force", False):
|
|
140
|
+
print("â Validation errors (use --force to ignore):", file=sys.stderr)
|
|
141
|
+
for e in errors:
|
|
142
|
+
print(f" {e.path}: {e.message}", file=sys.stderr)
|
|
143
|
+
return 1
|
|
144
|
+
|
|
145
|
+
ctx.build_dir.mkdir(parents=True, exist_ok=True)
|
|
146
|
+
|
|
147
|
+
# Run all generators in order
|
|
148
|
+
run_core_generators(spec, env_vars, ctx)
|
|
149
|
+
run_document_generators(spec, env_vars, ctx)
|
|
150
|
+
run_report_generators(spec, env_vars, ctx)
|
|
151
|
+
run_i18n_generators(spec, env_vars, ctx)
|
|
152
|
+
run_integration_generators(spec, env_vars, ctx)
|
|
153
|
+
run_workflow_generators(spec, env_vars, ctx)
|
|
154
|
+
run_ci_generator(spec, env_vars, ctx)
|
|
155
|
+
run_plugins(spec, env_vars, ctx)
|
|
156
|
+
|
|
157
|
+
write_lockfile(spec, ctx)
|
|
158
|
+
print(f"\nâ
Build complete â see {ctx.build_dir}/")
|
|
159
|
+
return 0
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""CLI command implementations.
|
|
2
|
+
|
|
3
|
+
Each module implements a single doql subcommand.
|
|
4
|
+
Commands are organized by functional area:
|
|
5
|
+
- Project lifecycle: init, validate, plan
|
|
6
|
+
- Build & deploy: build, sync, run, deploy
|
|
7
|
+
- Export: export, generate, render, query
|
|
8
|
+
- Utilities: docs, kiosk, quadlet
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from .init import cmd_init
|
|
13
|
+
from .validate import cmd_validate
|
|
14
|
+
from .plan import cmd_plan
|
|
15
|
+
from .run import cmd_run
|
|
16
|
+
from .deploy import cmd_deploy
|
|
17
|
+
from .export import cmd_export
|
|
18
|
+
from .generate import cmd_generate
|
|
19
|
+
from .render import cmd_render
|
|
20
|
+
from .query import cmd_query
|
|
21
|
+
from .kiosk import cmd_kiosk
|
|
22
|
+
from .quadlet import cmd_quadlet
|
|
23
|
+
from .docs import cmd_docs
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"cmd_init",
|
|
27
|
+
"cmd_validate",
|
|
28
|
+
"cmd_plan",
|
|
29
|
+
"cmd_run",
|
|
30
|
+
"cmd_deploy",
|
|
31
|
+
"cmd_export",
|
|
32
|
+
"cmd_generate",
|
|
33
|
+
"cmd_render",
|
|
34
|
+
"cmd_query",
|
|
35
|
+
"cmd_kiosk",
|
|
36
|
+
"cmd_quadlet",
|
|
37
|
+
"cmd_docs",
|
|
38
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Deploy command â deploy to environment."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import pathlib
|
|
6
|
+
|
|
7
|
+
from ...generators import deploy as deploy_gen
|
|
8
|
+
from ..context import BuildContext
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def cmd_deploy(args: argparse.Namespace) -> int:
|
|
12
|
+
"""Deploy project to target environment.
|
|
13
|
+
|
|
14
|
+
Delegates to infra_gen's deploy script.
|
|
15
|
+
"""
|
|
16
|
+
ctx = BuildContext(
|
|
17
|
+
root=pathlib.Path(getattr(args, "dir", None) or ".").resolve(),
|
|
18
|
+
doql_file=pathlib.Path(getattr(args, "dir", None) or ".").resolve() / (getattr(args, "file", None) or "app.doql"),
|
|
19
|
+
env_file=pathlib.Path(getattr(args, "dir", None) or ".").resolve() / ".env",
|
|
20
|
+
build_dir=pathlib.Path(getattr(args, "dir", None) or ".").resolve() / "build",
|
|
21
|
+
target_env=getattr(args, "env", None) or "prod",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
print(f"đ Deploying to {ctx.target_env}...")
|
|
25
|
+
return deploy_gen.run(ctx, target_env=ctx.target_env)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Docs command â generate documentation site."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import pathlib
|
|
6
|
+
|
|
7
|
+
from ... import parser as doql_parser
|
|
8
|
+
from ...generators import docs_gen
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def cmd_docs(args: argparse.Namespace) -> int:
|
|
12
|
+
"""Generate documentation site from .doql spec."""
|
|
13
|
+
root = pathlib.Path(getattr(args, "dir", None) or ".").resolve()
|
|
14
|
+
doql_file = root / (getattr(args, "file", None) or "app.doql")
|
|
15
|
+
|
|
16
|
+
spec = doql_parser.parse_file(doql_file)
|
|
17
|
+
out = root / "docs"
|
|
18
|
+
|
|
19
|
+
docs_gen.generate(spec, out)
|
|
20
|
+
print(f"đ Docs generated in {out}")
|
|
21
|
+
return 0
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Export command â export OpenAPI / Postman / TS SDK."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
import argparse
|
|
6
|
+
import pathlib
|
|
7
|
+
|
|
8
|
+
from ... import parser as doql_parser
|
|
9
|
+
from ...generators import api_gen, export_postman, export_ts_sdk
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def cmd_export(args: argparse.Namespace) -> int:
|
|
13
|
+
"""Export project specification to various formats.
|
|
14
|
+
|
|
15
|
+
Supported formats:
|
|
16
|
+
- openapi: OpenAPI 3.0 specification
|
|
17
|
+
- postman: Postman collection
|
|
18
|
+
- typescript-sdk: TypeScript client SDK
|
|
19
|
+
"""
|
|
20
|
+
root = pathlib.Path(getattr(args, "dir", None) or ".").resolve()
|
|
21
|
+
doql_file = root / (getattr(args, "file", None) or "app.doql")
|
|
22
|
+
|
|
23
|
+
fmt = args.format
|
|
24
|
+
spec = doql_parser.parse_file(doql_file)
|
|
25
|
+
|
|
26
|
+
if fmt == "openapi":
|
|
27
|
+
api_gen.export_openapi(spec, sys.stdout)
|
|
28
|
+
elif fmt == "postman":
|
|
29
|
+
export_postman.run(spec, sys.stdout)
|
|
30
|
+
elif fmt == "typescript-sdk":
|
|
31
|
+
export_ts_sdk.run(spec, sys.stdout)
|
|
32
|
+
else:
|
|
33
|
+
print(f"â Unknown format: {fmt}", file=sys.stderr)
|
|
34
|
+
return 1
|
|
35
|
+
|
|
36
|
+
return 0
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Generate command â generate a single document/artifact."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
import argparse
|
|
6
|
+
import pathlib
|
|
7
|
+
|
|
8
|
+
from ... import parser as doql_parser
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def cmd_generate(args: argparse.Namespace) -> int:
|
|
12
|
+
"""Generate a single document/artifact.
|
|
13
|
+
|
|
14
|
+
The artifact name must match a DOCUMENT defined in the .doql file.
|
|
15
|
+
"""
|
|
16
|
+
root = pathlib.Path(getattr(args, "dir", None) or ".").resolve()
|
|
17
|
+
doql_file = root / (getattr(args, "file", None) or "app.doql")
|
|
18
|
+
|
|
19
|
+
spec = doql_parser.parse_file(doql_file)
|
|
20
|
+
artifact = args.artifact
|
|
21
|
+
|
|
22
|
+
# Find matching DOCUMENT in spec
|
|
23
|
+
doc = next((d for d in spec.documents if d.name == artifact), None)
|
|
24
|
+
if not doc:
|
|
25
|
+
available = [d.name for d in spec.documents]
|
|
26
|
+
print(f"â Unknown artifact '{artifact}'. Available: {available}", file=sys.stderr)
|
|
27
|
+
return 1
|
|
28
|
+
|
|
29
|
+
print(f"đ Generating {artifact} ({doc.type})...")
|
|
30
|
+
# TODO: Faza 1 â Jinja2 + WeasyPrint pipeline
|
|
31
|
+
print(f" Template: {doc.template or '(none)'}")
|
|
32
|
+
print(f" Output: {doc.output or f'{artifact}.{doc.type}'}")
|
|
33
|
+
print("â ī¸ Generator not yet implemented â stub only.")
|
|
34
|
+
return 0
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Init command â create new project from template."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
import argparse
|
|
6
|
+
import pathlib
|
|
7
|
+
|
|
8
|
+
from ..context import scaffold_from_template
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def cmd_init(args: argparse.Namespace) -> int:
|
|
12
|
+
"""Create new project from template.
|
|
13
|
+
|
|
14
|
+
With --list-templates, shows available templates and exits.
|
|
15
|
+
"""
|
|
16
|
+
if getattr(args, "list_templates", False):
|
|
17
|
+
scaffolds_dir = pathlib.Path(__file__).parent.parent.parent / "scaffolds"
|
|
18
|
+
templates = sorted(p.name for p in scaffolds_dir.iterdir() if p.is_dir())
|
|
19
|
+
print("Available doql templates:")
|
|
20
|
+
for t in templates:
|
|
21
|
+
doql = scaffolds_dir / t / "app.doql"
|
|
22
|
+
first_line = ""
|
|
23
|
+
if doql.exists():
|
|
24
|
+
with doql.open() as fh:
|
|
25
|
+
first_line = fh.readline().strip()
|
|
26
|
+
print(f" {t:24} â {first_line}")
|
|
27
|
+
return 0
|
|
28
|
+
|
|
29
|
+
template = args.template or "minimal"
|
|
30
|
+
target = pathlib.Path(args.name)
|
|
31
|
+
if target.exists():
|
|
32
|
+
print(f"â Directory {target} already exists", file=sys.stderr)
|
|
33
|
+
return 1
|
|
34
|
+
|
|
35
|
+
print(f"đĻ Scaffolding {target} from template '{template}'...")
|
|
36
|
+
scaffold_from_template(template, target)
|
|
37
|
+
print(f"â
Created {target}/")
|
|
38
|
+
print(f" Next steps:")
|
|
39
|
+
print(f" cd {target}")
|
|
40
|
+
print(f" cp .env.example .env && $EDITOR .env")
|
|
41
|
+
print(f" doql validate")
|
|
42
|
+
print(f" doql build")
|
|
43
|
+
return 0
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Kiosk command â kiosk appliance management."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def cmd_kiosk(args: argparse.Namespace) -> int:
|
|
8
|
+
"""Manage kiosk appliance installation.
|
|
9
|
+
|
|
10
|
+
Use --install to set up kiosk mode on a Raspberry Pi.
|
|
11
|
+
"""
|
|
12
|
+
if args.install:
|
|
13
|
+
print("đĨī¸ Installing kiosk appliance...")
|
|
14
|
+
print(" Target: Raspberry Pi OS Lite 64-bit")
|
|
15
|
+
# TODO: Faza 2 â Openbox autostart, chromium --kiosk, udev rules, systemd
|
|
16
|
+
print("â ī¸ Kiosk installer not yet implemented â stub only.")
|
|
17
|
+
return 0
|
|
18
|
+
|
|
19
|
+
print("âšī¸ Use --install to set up kiosk on this device.")
|
|
20
|
+
return 0
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Plan command â dry-run showing what would be generated."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import pathlib
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from ... import parsers as doql_parser
|
|
9
|
+
from ..context import estimate_file_count
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from ...parsers.models import DoqlSpec
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _print_header(spec: DoqlSpec) -> None:
|
|
16
|
+
"""Print plan header with app name and version."""
|
|
17
|
+
print(f"đ Plan for {spec.app_name} (v{spec.version}):\n")
|
|
18
|
+
print(f" Domain: {spec.domain or '(not set)'}")
|
|
19
|
+
print(f" Languages: {spec.languages or ['(default)']}")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _print_entities(spec: DoqlSpec) -> None:
|
|
23
|
+
"""Print entities section."""
|
|
24
|
+
print(f" Entities: {len(spec.entities)}")
|
|
25
|
+
for e in spec.entities:
|
|
26
|
+
print(f" âĸ {e.name} ({len(e.fields)} fields)")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _print_data_sources(spec: DoqlSpec) -> None:
|
|
30
|
+
"""Print DATA sources section."""
|
|
31
|
+
print(f" DATA sources: {len(spec.data_sources)}")
|
|
32
|
+
for d in spec.data_sources:
|
|
33
|
+
file_info = f" â {d.file}" if d.file else ""
|
|
34
|
+
print(f" âĸ {d.name} ({d.source}{file_info})")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _print_documents(spec: DoqlSpec) -> None:
|
|
38
|
+
"""Print documents and templates section."""
|
|
39
|
+
print(f" Templates: {len(spec.templates)}")
|
|
40
|
+
print(f" Documents: {len(spec.documents)}")
|
|
41
|
+
for d in spec.documents:
|
|
42
|
+
print(f" âĸ {d.name} ({d.type})")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _print_api_clients(spec: DoqlSpec) -> None:
|
|
46
|
+
"""Print API clients section."""
|
|
47
|
+
print(f" API clients: {len(spec.api_clients)}")
|
|
48
|
+
for a in spec.api_clients:
|
|
49
|
+
print(f" âĸ {a.name} ({a.base_url or ' ?'})")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _print_interfaces(spec: DoqlSpec) -> None:
|
|
53
|
+
"""Print interfaces section."""
|
|
54
|
+
print(f" Interfaces: {[i.name for i in spec.interfaces]}")
|
|
55
|
+
for i in spec.interfaces:
|
|
56
|
+
pages = [p.name for p in i.pages]
|
|
57
|
+
print(f" âĸ {i.name} ({i.type}) pages={pages}")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _print_workflows(spec: DoqlSpec) -> None:
|
|
61
|
+
"""Print workflows section."""
|
|
62
|
+
print(f" Workflows: {len(spec.workflows)}")
|
|
63
|
+
for w in spec.workflows:
|
|
64
|
+
trigger = w.trigger or w.schedule or '?'
|
|
65
|
+
print(f" âĸ {w.name} (trigger={trigger})")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _print_summary(spec: DoqlSpec) -> None:
|
|
69
|
+
"""Print summary statistics."""
|
|
70
|
+
print(f" Reports: {len(spec.reports)}")
|
|
71
|
+
print(f" Databases: {len(spec.databases)}")
|
|
72
|
+
print(f" Webhooks: {len(spec.webhooks)}")
|
|
73
|
+
print(f" Scenarios: {len(spec.scenarios)}")
|
|
74
|
+
print(f" Integrations: {[ig.name for ig in spec.integrations]}")
|
|
75
|
+
print(f" Roles: {[r.name for r in spec.roles]}")
|
|
76
|
+
print(f" Deploy target: {spec.deploy.target if spec.deploy else '(none)'}")
|
|
77
|
+
print(f" Env references: {len(spec.env_refs)} vars")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _print_file_counts(spec: DoqlSpec) -> None:
|
|
81
|
+
"""Print estimated file generation counts."""
|
|
82
|
+
print("\n Files to generate:")
|
|
83
|
+
total = 0
|
|
84
|
+
for iface in spec.interfaces:
|
|
85
|
+
count = estimate_file_count(iface)
|
|
86
|
+
total += count
|
|
87
|
+
print(f" {iface.name:12} ~{count} files")
|
|
88
|
+
|
|
89
|
+
infra_count = len(spec.integrations) + 3
|
|
90
|
+
print(f" infra ~{infra_count} files")
|
|
91
|
+
print(f" TOTAL: ~{total + infra_count} files\n")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def cmd_plan(args: argparse.Namespace) -> int:
|
|
95
|
+
"""Show dry-run plan of what would be generated.
|
|
96
|
+
|
|
97
|
+
Displays project overview including entities, data sources, interfaces,
|
|
98
|
+
and estimated file counts per interface type.
|
|
99
|
+
"""
|
|
100
|
+
root = pathlib.Path(getattr(args, "dir", None) or ".").resolve()
|
|
101
|
+
doql_file = root / (getattr(args, "file", None) or "app.doql")
|
|
102
|
+
|
|
103
|
+
spec = doql_parser.parse_file(doql_file)
|
|
104
|
+
|
|
105
|
+
_print_header(spec)
|
|
106
|
+
_print_entities(spec)
|
|
107
|
+
_print_data_sources(spec)
|
|
108
|
+
_print_documents(spec)
|
|
109
|
+
_print_api_clients(spec)
|
|
110
|
+
_print_summary(spec)
|
|
111
|
+
_print_interfaces(spec)
|
|
112
|
+
_print_workflows(spec)
|
|
113
|
+
_print_file_counts(spec)
|
|
114
|
+
|
|
115
|
+
return 0
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Quadlet command â Podman Quadlet management."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import pathlib
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def cmd_quadlet(args: argparse.Namespace) -> int:
|
|
9
|
+
"""Manage Podman Quadlet containers.
|
|
10
|
+
|
|
11
|
+
Use --install to deploy Quadlet container definitions to systemd.
|
|
12
|
+
"""
|
|
13
|
+
if args.install:
|
|
14
|
+
print("đŗ Installing Quadlet containers to systemd...")
|
|
15
|
+
quadlet_dir = pathlib.Path.home() / ".config" / "containers" / "systemd"
|
|
16
|
+
print(f" Target: {quadlet_dir}")
|
|
17
|
+
# TODO: Faza 1 â copy .container files, systemctl --user daemon-reload
|
|
18
|
+
print("â ī¸ Quadlet installer not yet implemented â stub only.")
|
|
19
|
+
return 0
|
|
20
|
+
|
|
21
|
+
print("âšī¸ Use --install to deploy Quadlet containers to systemd.")
|
|
22
|
+
return 0
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Query command â query a DATA source â JSON."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
import argparse
|
|
6
|
+
import pathlib
|
|
7
|
+
|
|
8
|
+
from ... import parser as doql_parser
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def cmd_query(args: argparse.Namespace) -> int:
|
|
12
|
+
"""Query a DATA source and output as JSON.
|
|
13
|
+
|
|
14
|
+
Supports JSON, SQLite, CSV, Excel, and API data sources.
|
|
15
|
+
"""
|
|
16
|
+
root = pathlib.Path(getattr(args, "dir", None) or ".").resolve()
|
|
17
|
+
doql_file = root / (getattr(args, "file", None) or "app.doql")
|
|
18
|
+
data_name = args.data
|
|
19
|
+
|
|
20
|
+
spec = doql_parser.parse_file(doql_file)
|
|
21
|
+
ds = next((d for d in spec.data_sources if d.name == data_name), None)
|
|
22
|
+
|
|
23
|
+
if not ds:
|
|
24
|
+
available = [d.name for d in spec.data_sources]
|
|
25
|
+
print(f"â Unknown DATA source '{data_name}'. Available: {available}", file=sys.stderr)
|
|
26
|
+
return 1
|
|
27
|
+
|
|
28
|
+
print(f"đ Querying DATA {data_name} (source: {ds.source})...")
|
|
29
|
+
# TODO: Faza 1 â load JSON/SQLite/API and output as JSON
|
|
30
|
+
print("â ī¸ Query engine not yet implemented â stub only.")
|
|
31
|
+
return 0
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Render command â render a template with data."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
import argparse
|
|
6
|
+
import pathlib
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def cmd_render(args: argparse.Namespace) -> int:
|
|
10
|
+
"""Render a template with DATA sources.
|
|
11
|
+
|
|
12
|
+
Template path can be relative to current directory or project root.
|
|
13
|
+
"""
|
|
14
|
+
root = pathlib.Path(getattr(args, "dir", None) or ".").resolve()
|
|
15
|
+
template_path = pathlib.Path(args.template)
|
|
16
|
+
|
|
17
|
+
if not template_path.exists():
|
|
18
|
+
template_path = root / args.template
|
|
19
|
+
if not template_path.exists():
|
|
20
|
+
print(f"â Template not found: {args.template}", file=sys.stderr)
|
|
21
|
+
return 1
|
|
22
|
+
|
|
23
|
+
print(f"đ¨ Rendering {template_path.name}...")
|
|
24
|
+
# TODO: Faza 1 â Jinja2 render with DATA sources
|
|
25
|
+
print("â ī¸ Renderer not yet implemented â stub only.")
|
|
26
|
+
return 0
|
doql/cli/commands/run.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Run command â run locally in dev mode."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
import subprocess
|
|
6
|
+
import argparse
|
|
7
|
+
import pathlib
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def cmd_run(args: argparse.Namespace) -> int:
|
|
11
|
+
"""Run project locally in dev mode using docker-compose.
|
|
12
|
+
|
|
13
|
+
Requires existing build (run `doql build` first).
|
|
14
|
+
"""
|
|
15
|
+
root = pathlib.Path(getattr(args, "dir", None) or ".").resolve()
|
|
16
|
+
build_dir = root / "build"
|
|
17
|
+
compose = build_dir / "infra" / "docker-compose.yml"
|
|
18
|
+
|
|
19
|
+
if not compose.exists():
|
|
20
|
+
print("â No build found. Run `doql build` first.", file=sys.stderr)
|
|
21
|
+
return 1
|
|
22
|
+
|
|
23
|
+
return subprocess.call([
|
|
24
|
+
"docker", "compose", "-f", str(compose), "up", "--build"
|
|
25
|
+
])
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Validate command â validate .doql + .env."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
import argparse
|
|
6
|
+
import pathlib
|
|
7
|
+
|
|
8
|
+
from ... import parser as doql_parser
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def cmd_validate(args: argparse.Namespace) -> int:
|
|
12
|
+
"""Validate .doql file and .env configuration.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
0 if validation passes, 1 if there are errors
|
|
16
|
+
"""
|
|
17
|
+
root = pathlib.Path(getattr(args, "dir", None) or ".").resolve()
|
|
18
|
+
doql_file = root / (getattr(args, "file", None) or "app.doql")
|
|
19
|
+
env_file = root / ".env"
|
|
20
|
+
|
|
21
|
+
print(f"đ Validating {doql_file}...")
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
spec = doql_parser.parse_file(doql_file)
|
|
25
|
+
env_vars = doql_parser.parse_env(env_file)
|
|
26
|
+
issues = doql_parser.validate(spec, env_vars, project_root=root)
|
|
27
|
+
except doql_parser.DoqlParseError as e:
|
|
28
|
+
print(f"â Parse error: {e}", file=sys.stderr)
|
|
29
|
+
return 1
|
|
30
|
+
|
|
31
|
+
if not issues:
|
|
32
|
+
print("â
Everything looks good.")
|
|
33
|
+
return 0
|
|
34
|
+
|
|
35
|
+
errors = sum(1 for i in issues if i.severity == "error")
|
|
36
|
+
warnings = sum(1 for i in issues if i.severity == "warning")
|
|
37
|
+
|
|
38
|
+
for issue in issues:
|
|
39
|
+
level = "â" if issue.severity == "error" else "â ī¸ "
|
|
40
|
+
print(f"{level} {issue.path}: {issue.message}")
|
|
41
|
+
|
|
42
|
+
print(f"\n {errors} error(s), {warnings} warning(s)")
|
|
43
|
+
return 1 if errors > 0 else 0
|