jac-scale 0.1.1__py3-none-any.whl → 0.1.3__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.
- jac_scale/abstractions/config/app_config.jac +5 -2
- jac_scale/context.jac +2 -1
- jac_scale/factories/storage_factory.jac +75 -0
- jac_scale/google_sso_provider.jac +85 -0
- jac_scale/impl/context.impl.jac +3 -0
- jac_scale/impl/serve.impl.jac +82 -234
- jac_scale/impl/user_manager.impl.jac +349 -0
- jac_scale/memory_hierarchy.jac +3 -1
- jac_scale/plugin.jac +46 -3
- jac_scale/plugin_config.jac +27 -0
- jac_scale/serve.jac +3 -12
- jac_scale/sso_provider.jac +72 -0
- jac_scale/targets/kubernetes/kubernetes_config.jac +9 -15
- jac_scale/targets/kubernetes/kubernetes_target.jac +174 -15
- jac_scale/tests/fixtures/scale-feats/components/Button.cl.jac +32 -0
- jac_scale/tests/fixtures/scale-feats/main.jac +147 -0
- jac_scale/tests/fixtures/test_api.jac +29 -0
- jac_scale/tests/fixtures/test_restspec.jac +37 -0
- jac_scale/tests/test_deploy_k8s.py +2 -1
- jac_scale/tests/test_examples.py +180 -5
- jac_scale/tests/test_hooks.py +39 -0
- jac_scale/tests/test_restspec.py +192 -0
- jac_scale/tests/test_serve.py +54 -0
- jac_scale/tests/test_sso.py +273 -284
- jac_scale/tests/test_storage.py +274 -0
- jac_scale/user_manager.jac +49 -0
- {jac_scale-0.1.1.dist-info → jac_scale-0.1.3.dist-info}/METADATA +9 -2
- {jac_scale-0.1.1.dist-info → jac_scale-0.1.3.dist-info}/RECORD +31 -20
- {jac_scale-0.1.1.dist-info → jac_scale-0.1.3.dist-info}/WHEEL +1 -1
- {jac_scale-0.1.1.dist-info → jac_scale-0.1.3.dist-info}/entry_points.txt +0 -0
- {jac_scale-0.1.1.dist-info → jac_scale-0.1.3.dist-info}/top_level.txt +0 -0
|
@@ -174,10 +174,7 @@ class KubernetesTarget(DeploymentTarget) {
|
|
|
174
174
|
return service;
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
"""Build the bash command for setting up Jaseci runtime environment.
|
|
178
|
-
|
|
179
|
-
This replaces the hard-coded command with a configurable version.
|
|
180
|
-
"""
|
|
177
|
+
"""Build the bash command for setting up Jaseci runtime environment."""
|
|
181
178
|
def _build_runtime_setup_command(
|
|
182
179
|
self: KubernetesTarget, app_config: AppConfig
|
|
183
180
|
) -> list[str] {
|
|
@@ -188,36 +185,61 @@ class KubernetesTarget(DeploymentTarget) {
|
|
|
188
185
|
commands.append('export DEBIAN_FRONTEND=noninteractive');
|
|
189
186
|
commands.append('apt-get update');
|
|
190
187
|
|
|
191
|
-
# Install base packages
|
|
192
|
-
base_packages = ['git', '
|
|
188
|
+
# Install base packages (curl and unzip needed for Bun installation)
|
|
189
|
+
base_packages = ['git', 'curl', 'unzip'];
|
|
193
190
|
if config.additional_packages {
|
|
194
191
|
base_packages.extend(config.additional_packages);
|
|
195
192
|
}
|
|
196
193
|
commands.append(f"apt-get install -y {' '.join(base_packages)}");
|
|
197
194
|
|
|
198
|
-
#
|
|
199
|
-
|
|
195
|
+
# Install Bun (required for jac-client frontend builds)
|
|
196
|
+
commands.append('curl -fsSL https://bun.sh/install | bash');
|
|
197
|
+
commands.append('export BUN_INSTALL="$HOME/.bun"');
|
|
198
|
+
commands.append('export PATH="$BUN_INSTALL/bin:$PATH"');
|
|
199
|
+
|
|
200
|
+
if app_config.experimental {
|
|
200
201
|
commands.append('rm -rf jaseci');
|
|
201
|
-
# Clone repository
|
|
202
202
|
clone_cmd = f"git clone --branch {config.jaseci_branch} --single-branch {config.jaseci_repo_url}";
|
|
203
203
|
commands.append(clone_cmd);
|
|
204
204
|
commands.append('cd ./jaseci');
|
|
205
|
-
# Checkout specific commit if provided
|
|
206
205
|
if config.jaseci_commit {
|
|
207
206
|
commands.append(f"git checkout {config.jaseci_commit}");
|
|
208
207
|
}
|
|
209
|
-
# Update submodules
|
|
210
208
|
commands.append('git submodule update --init --recursive');
|
|
211
|
-
# Setup virtual environment
|
|
212
|
-
commands.append('python -m venv venv');
|
|
213
|
-
commands.append('source venv/bin/activate');
|
|
214
|
-
# Install Jaseci components
|
|
215
209
|
commands.append('pip install pluggy');
|
|
216
210
|
commands.append('pip install -e ./jac');
|
|
217
211
|
commands.append('pip install -e ./jac-scale');
|
|
218
212
|
commands.append('pip install -e ./jac-client');
|
|
219
213
|
commands.append('pip install -e ./jac-byllm');
|
|
220
214
|
commands.append('cd ..');
|
|
215
|
+
} else {
|
|
216
|
+
packages = config.plugin_versions or {};
|
|
217
|
+
jaclang_v = packages.get('jaclang', 'latest');
|
|
218
|
+
scale_v = packages.get('jac_scale', 'latest');
|
|
219
|
+
client_v = packages.get('jac_client', 'latest');
|
|
220
|
+
byllm_v = packages.get('jac_byllm', 'latest');
|
|
221
|
+
if jaclang_v == 'latest' {
|
|
222
|
+
commands.append('pip install jaclang');
|
|
223
|
+
} else {
|
|
224
|
+
commands.append(f'pip install jaclang=={jaclang_v}');
|
|
225
|
+
}
|
|
226
|
+
if scale_v == 'latest' {
|
|
227
|
+
commands.append('pip install jac-scale');
|
|
228
|
+
} else {
|
|
229
|
+
commands.append(f'pip install jac-scale=={scale_v}');
|
|
230
|
+
}
|
|
231
|
+
if client_v == 'latest' {
|
|
232
|
+
commands.append('pip install jac-client');
|
|
233
|
+
} else {
|
|
234
|
+
commands.append(f'pip install jac-client=={client_v}');
|
|
235
|
+
}
|
|
236
|
+
if byllm_v and byllm_v != 'none' {
|
|
237
|
+
if byllm_v == 'latest' {
|
|
238
|
+
commands.append('pip install byllm');
|
|
239
|
+
} else {
|
|
240
|
+
commands.append(f'pip install byllm=={byllm_v}');
|
|
241
|
+
}
|
|
242
|
+
}
|
|
221
243
|
}
|
|
222
244
|
|
|
223
245
|
# Change to app directory (project is already copied there via volume mount)
|
|
@@ -641,6 +663,135 @@ class KubernetesTarget(DeploymentTarget) {
|
|
|
641
663
|
);
|
|
642
664
|
}
|
|
643
665
|
|
|
666
|
+
"""Wait for resources to be completely deleted."""
|
|
667
|
+
def _wait_for_deletion(
|
|
668
|
+
self: KubernetesTarget,
|
|
669
|
+
app_name: str,
|
|
670
|
+
namespace: str,
|
|
671
|
+
apps_v1: Any,
|
|
672
|
+
core_v1: Any,
|
|
673
|
+
max_wait: int = 60,
|
|
674
|
+
poll_interval: float = 1.0
|
|
675
|
+
) -> None {
|
|
676
|
+
elapsed = 0.0;
|
|
677
|
+
while elapsed < max_wait {
|
|
678
|
+
resources_exist = False;
|
|
679
|
+
|
|
680
|
+
# Check deployment
|
|
681
|
+
try {
|
|
682
|
+
apps_v1.read_namespaced_deployment(name=app_name, namespace=namespace);
|
|
683
|
+
resources_exist = True;
|
|
684
|
+
} except ApiException as e {
|
|
685
|
+
if e.status != 404 {
|
|
686
|
+
raise ;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
# Check service
|
|
691
|
+
try {
|
|
692
|
+
core_v1.read_namespaced_service(
|
|
693
|
+
name=f"{app_name}-service", namespace=namespace
|
|
694
|
+
);
|
|
695
|
+
resources_exist = True;
|
|
696
|
+
} except ApiException as e {
|
|
697
|
+
if e.status != 404 {
|
|
698
|
+
raise ;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
# Check MongoDB resources if enabled
|
|
703
|
+
if self.k8s_config.mongodb_enabled {
|
|
704
|
+
mongodb_name = f"{app_name}-mongodb";
|
|
705
|
+
try {
|
|
706
|
+
apps_v1.read_namespaced_stateful_set(
|
|
707
|
+
name=mongodb_name, namespace=namespace
|
|
708
|
+
);
|
|
709
|
+
resources_exist = True;
|
|
710
|
+
} except ApiException as e {
|
|
711
|
+
if e.status != 404 {
|
|
712
|
+
raise ;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
try {
|
|
716
|
+
core_v1.read_namespaced_service(
|
|
717
|
+
name=f"{mongodb_name}-service", namespace=namespace
|
|
718
|
+
);
|
|
719
|
+
resources_exist = True;
|
|
720
|
+
} except ApiException as e {
|
|
721
|
+
if e.status != 404 {
|
|
722
|
+
raise ;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
# Check Redis resources if enabled
|
|
728
|
+
if self.k8s_config.redis_enabled {
|
|
729
|
+
redis_name = f"{app_name}-redis";
|
|
730
|
+
try {
|
|
731
|
+
apps_v1.read_namespaced_deployment(
|
|
732
|
+
name=redis_name, namespace=namespace
|
|
733
|
+
);
|
|
734
|
+
resources_exist = True;
|
|
735
|
+
} except ApiException as e {
|
|
736
|
+
if e.status != 404 {
|
|
737
|
+
raise ;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
try {
|
|
741
|
+
core_v1.read_namespaced_service(
|
|
742
|
+
name=f"{redis_name}-service", namespace=namespace
|
|
743
|
+
);
|
|
744
|
+
resources_exist = True;
|
|
745
|
+
} except ApiException as e {
|
|
746
|
+
if e.status != 404 {
|
|
747
|
+
raise ;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
# Check PVCs
|
|
753
|
+
try {
|
|
754
|
+
pvcs = core_v1.list_namespaced_persistent_volume_claim(namespace);
|
|
755
|
+
for pvc in pvcs.items {
|
|
756
|
+
if pvc.metadata.name.startswith(app_name) {
|
|
757
|
+
resources_exist = True;
|
|
758
|
+
break;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
} except Exception { }
|
|
762
|
+
|
|
763
|
+
# Check code sync pod
|
|
764
|
+
try {
|
|
765
|
+
core_v1.read_namespaced_pod(
|
|
766
|
+
name=f"{app_name}-code-sync", namespace=namespace
|
|
767
|
+
);
|
|
768
|
+
resources_exist = True;
|
|
769
|
+
} except ApiException as e {
|
|
770
|
+
if e.status != 404 {
|
|
771
|
+
raise ;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
# If no resources exist, deletion is complete
|
|
776
|
+
if not resources_exist {
|
|
777
|
+
if self.logger {
|
|
778
|
+
self.logger.info(
|
|
779
|
+
f"All resources for '{app_name}' have been deleted"
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
time.sleep(poll_interval);
|
|
785
|
+
elapsed = elapsed + poll_interval;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if self.logger {
|
|
789
|
+
self.logger.warn(
|
|
790
|
+
f"Timeout waiting for resources to be deleted after {max_wait} seconds"
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
644
795
|
"""Destroy all enabled databases."""
|
|
645
796
|
def _destroy_databases(
|
|
646
797
|
self: KubernetesTarget,
|
|
@@ -727,6 +878,14 @@ class KubernetesTarget(DeploymentTarget) {
|
|
|
727
878
|
}
|
|
728
879
|
}
|
|
729
880
|
|
|
881
|
+
# Wait for all resources to be completely deleted
|
|
882
|
+
if self.logger {
|
|
883
|
+
self.logger.info(
|
|
884
|
+
f"Waiting for all resources to be deleted for '{app_name}'..."
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
self._wait_for_deletion(app_name, namespace, apps_v1, core_v1);
|
|
888
|
+
|
|
730
889
|
if self.logger {
|
|
731
890
|
self.logger.info(f"Application '{app_name}' destroyed successfully");
|
|
732
891
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Button component for the Jac client application."""
|
|
2
|
+
|
|
3
|
+
def:pub Button(label: str, onClick: any, variant: str = "primary", disabled: bool = False) -> any {
|
|
4
|
+
base_styles = {
|
|
5
|
+
"padding": "0.75rem 1.5rem",
|
|
6
|
+
"fontSize": "1rem",
|
|
7
|
+
"fontWeight": "600",
|
|
8
|
+
"borderRadius": "0.5rem",
|
|
9
|
+
"border": "none",
|
|
10
|
+
"cursor": "not-allowed" if disabled else "pointer",
|
|
11
|
+
"transition": "all 0.2s ease"
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
variant_styles = {
|
|
15
|
+
"primary": {
|
|
16
|
+
"backgroundColor": "#9ca3af" if disabled else "#3b82f6",
|
|
17
|
+
"color": "#ffffff"
|
|
18
|
+
},
|
|
19
|
+
"secondary": {
|
|
20
|
+
"backgroundColor": "#e5e7eb" if disabled else "#6b7280",
|
|
21
|
+
"color": "#ffffff"
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return <button
|
|
26
|
+
style={{**base_styles, **variant_styles[variant]}}
|
|
27
|
+
onClick={onClick}
|
|
28
|
+
disabled={disabled}
|
|
29
|
+
>
|
|
30
|
+
{label}
|
|
31
|
+
</button>;
|
|
32
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""jac-scale features test fixture.
|
|
2
|
+
|
|
3
|
+
Demonstrates jac-scale specific features that require FastAPI:
|
|
4
|
+
- Storage abstraction with file uploads
|
|
5
|
+
- UploadFile handling
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import from fastapi { UploadFile }
|
|
9
|
+
import from uuid { uuid4 }
|
|
10
|
+
|
|
11
|
+
# Initialize storage with custom base path
|
|
12
|
+
glob storage = store(base_path="./uploads");
|
|
13
|
+
|
|
14
|
+
# ============================================================================
|
|
15
|
+
# Storage API Walkers
|
|
16
|
+
# ============================================================================
|
|
17
|
+
|
|
18
|
+
"""Upload a file to storage."""
|
|
19
|
+
walker:pub upload_file {
|
|
20
|
+
has file: UploadFile;
|
|
21
|
+
has folder: str = "documents";
|
|
22
|
+
|
|
23
|
+
can process with `root entry {
|
|
24
|
+
# Generate unique filename
|
|
25
|
+
ext = self.file.filename.rsplit(".", 1)[-1] if "." in self.file.filename else "";
|
|
26
|
+
unique_name = f"{uuid4()}.{ext}" if ext else str(uuid4());
|
|
27
|
+
path = f"{self.folder}/{unique_name}";
|
|
28
|
+
|
|
29
|
+
# Upload file to storage
|
|
30
|
+
storage.upload(self.file.file, path);
|
|
31
|
+
|
|
32
|
+
# Get metadata
|
|
33
|
+
metadata = storage.get_metadata(path);
|
|
34
|
+
|
|
35
|
+
report {
|
|
36
|
+
"success": True,
|
|
37
|
+
"original_filename": self.file.filename,
|
|
38
|
+
"storage_path": path,
|
|
39
|
+
"size": metadata["size"],
|
|
40
|
+
"content_type": self.file.content_type
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
"""List files in storage."""
|
|
46
|
+
walker:pub list_files {
|
|
47
|
+
has folder: str = "";
|
|
48
|
+
has recursive: bool = False;
|
|
49
|
+
|
|
50
|
+
can process with `root entry {
|
|
51
|
+
files = [];
|
|
52
|
+
for path in storage.list_files(self.folder, self.recursive) {
|
|
53
|
+
metadata = storage.get_metadata(path);
|
|
54
|
+
files.append({
|
|
55
|
+
"path": path,
|
|
56
|
+
"size": metadata["size"],
|
|
57
|
+
"modified": str(metadata["modified"])
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
report {"files": files, "count": len(files)};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
"""Delete a file from storage."""
|
|
65
|
+
walker:pub delete_file {
|
|
66
|
+
has path: str;
|
|
67
|
+
|
|
68
|
+
can process with `root entry {
|
|
69
|
+
if not storage.exists(self.path) {
|
|
70
|
+
report {"success": False, "error": "File not found"};
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
deleted = storage.delete(self.path);
|
|
74
|
+
report {"success": deleted, "path": self.path};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
"""Download a file from storage."""
|
|
79
|
+
walker:pub download_file {
|
|
80
|
+
has path: str;
|
|
81
|
+
|
|
82
|
+
can process with `root entry {
|
|
83
|
+
if not storage.exists(self.path) {
|
|
84
|
+
report {"success": False, "error": "File not found"};
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
content = storage.download(self.path);
|
|
88
|
+
metadata = storage.get_metadata(self.path);
|
|
89
|
+
report {
|
|
90
|
+
"success": True,
|
|
91
|
+
"path": self.path,
|
|
92
|
+
"size": metadata["size"],
|
|
93
|
+
"content_length": len(content) if content else 0
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
"""Check if a file exists in storage."""
|
|
99
|
+
walker:pub file_exists {
|
|
100
|
+
has path: str;
|
|
101
|
+
|
|
102
|
+
can process with `root entry {
|
|
103
|
+
exists = storage.exists(self.path);
|
|
104
|
+
report {"path": self.path, "exists": exists};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
"""Copy a file within storage."""
|
|
109
|
+
walker:pub copy_file {
|
|
110
|
+
has source: str;
|
|
111
|
+
has destination: str;
|
|
112
|
+
|
|
113
|
+
can process with `root entry {
|
|
114
|
+
if not storage.exists(self.source) {
|
|
115
|
+
report {"success": False, "error": "Source file not found"};
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
result = storage.copy(self.source, self.destination);
|
|
119
|
+
report {"success": result, "source": self.source, "destination": self.destination};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
"""Move a file within storage."""
|
|
124
|
+
walker:pub move_file {
|
|
125
|
+
has source: str;
|
|
126
|
+
has destination: str;
|
|
127
|
+
|
|
128
|
+
can process with `root entry {
|
|
129
|
+
if not storage.exists(self.source) {
|
|
130
|
+
report {"success": False, "error": "Source file not found"};
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
result = storage.move(self.source, self.destination);
|
|
134
|
+
report {"success": result, "source": self.source, "destination": self.destination};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# ============================================================================
|
|
139
|
+
# Health Check
|
|
140
|
+
# ============================================================================
|
|
141
|
+
|
|
142
|
+
"""Simple health check walker."""
|
|
143
|
+
walker:pub health {
|
|
144
|
+
can check with `root entry {
|
|
145
|
+
report {"status": "ok", "features": ["storage", "file_upload"]};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -89,6 +89,35 @@ walker : pub PublicInfo {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
# Streaming Walker Test
|
|
93
|
+
walker : pub WalkerStream {
|
|
94
|
+
has count: int = 3;
|
|
95
|
+
|
|
96
|
+
can stream_reports with `root entry {
|
|
97
|
+
def stream -> str {
|
|
98
|
+
import time;
|
|
99
|
+
for i in range(self.count) {
|
|
100
|
+
time.sleep(0.5);
|
|
101
|
+
yield f"Report {i}";
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
report stream();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
import from typing { Generator }
|
|
109
|
+
# Streaming function test
|
|
110
|
+
def : pub FunctionStream(count: int = 3) -> Generator {
|
|
111
|
+
import time;
|
|
112
|
+
def stream -> Generator {
|
|
113
|
+
for i in range(count) {
|
|
114
|
+
time.sleep(0.5);
|
|
115
|
+
yield f"Func {i}";
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
report stream();
|
|
119
|
+
}
|
|
120
|
+
|
|
92
121
|
# Async Walker Test
|
|
93
122
|
import asyncio;
|
|
94
123
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
|
|
2
|
+
"""Test API fixture for restspec decorator tests."""
|
|
3
|
+
|
|
4
|
+
import from jaclang.runtimelib.builtin { restspec }
|
|
5
|
+
import from http { HTTPMethod }
|
|
6
|
+
|
|
7
|
+
# ============================================================================
|
|
8
|
+
# RestSpec Decorator Tests
|
|
9
|
+
# ============================================================================
|
|
10
|
+
|
|
11
|
+
"""Walker with GET method via restspec decorator."""
|
|
12
|
+
@restspec(method=HTTPMethod.GET)
|
|
13
|
+
walker : pub GetWalker {
|
|
14
|
+
can get_data with `root entry {
|
|
15
|
+
report {"message": "GetWalker executed", "method": "GET"};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
"""Walker with custom path."""
|
|
20
|
+
@restspec(method=HTTPMethod.GET, path="/custom/walker")
|
|
21
|
+
walker : pub CustomPathWalker {
|
|
22
|
+
can get_data with `root entry {
|
|
23
|
+
report {"message": "CustomPathWalker executed", "path": "/custom/walker"};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
"""Function with GET method via restspec decorator."""
|
|
28
|
+
@restspec(method=HTTPMethod.GET)
|
|
29
|
+
def get_func() -> dict {
|
|
30
|
+
return {"message": "get_func executed", "method": "GET"};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
"""Function with custom path."""
|
|
34
|
+
@restspec(method=HTTPMethod.GET, path="/custom/func")
|
|
35
|
+
def custom_path_func() -> dict {
|
|
36
|
+
return {"message": "custom_path_func executed", "path": "/custom/func"};
|
|
37
|
+
}
|
|
@@ -97,10 +97,12 @@ def test_deploy_all_in_one():
|
|
|
97
97
|
)
|
|
98
98
|
|
|
99
99
|
# Create app config
|
|
100
|
+
# Use experimental=True to install from repo (PyPI packages may not be available)
|
|
100
101
|
app_config = AppConfig(
|
|
101
102
|
code_folder=todo_app_path,
|
|
102
103
|
file_name="main.jac",
|
|
103
104
|
build=False,
|
|
105
|
+
experimental=True,
|
|
104
106
|
)
|
|
105
107
|
|
|
106
108
|
# Deploy using new architecture
|
|
@@ -172,7 +174,6 @@ def test_deploy_all_in_one():
|
|
|
172
174
|
|
|
173
175
|
# Cleanup using new architecture
|
|
174
176
|
deployment_target.destroy(app_name)
|
|
175
|
-
time.sleep(60) # Wait for deletion to propagate
|
|
176
177
|
|
|
177
178
|
# Verify cleanup - resources should no longer exist
|
|
178
179
|
try:
|