jac-scale 0.1.1__py3-none-any.whl → 0.1.4__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 (36) hide show
  1. jac_scale/abstractions/config/app_config.jac +5 -2
  2. jac_scale/config_loader.jac +2 -1
  3. jac_scale/context.jac +2 -1
  4. jac_scale/factories/storage_factory.jac +75 -0
  5. jac_scale/google_sso_provider.jac +85 -0
  6. jac_scale/impl/config_loader.impl.jac +28 -3
  7. jac_scale/impl/context.impl.jac +1 -0
  8. jac_scale/impl/serve.impl.jac +749 -266
  9. jac_scale/impl/user_manager.impl.jac +349 -0
  10. jac_scale/impl/webhook.impl.jac +212 -0
  11. jac_scale/jserver/impl/jfast_api.impl.jac +4 -0
  12. jac_scale/memory_hierarchy.jac +3 -1
  13. jac_scale/plugin.jac +46 -3
  14. jac_scale/plugin_config.jac +28 -1
  15. jac_scale/serve.jac +33 -16
  16. jac_scale/sso_provider.jac +72 -0
  17. jac_scale/targets/kubernetes/kubernetes_config.jac +9 -15
  18. jac_scale/targets/kubernetes/kubernetes_target.jac +174 -15
  19. jac_scale/tests/fixtures/scale-feats/components/Button.cl.jac +32 -0
  20. jac_scale/tests/fixtures/scale-feats/main.jac +147 -0
  21. jac_scale/tests/fixtures/test_api.jac +89 -0
  22. jac_scale/tests/fixtures/test_restspec.jac +88 -0
  23. jac_scale/tests/test_deploy_k8s.py +2 -1
  24. jac_scale/tests/test_examples.py +180 -5
  25. jac_scale/tests/test_hooks.py +39 -0
  26. jac_scale/tests/test_restspec.py +289 -0
  27. jac_scale/tests/test_serve.py +411 -4
  28. jac_scale/tests/test_sso.py +273 -284
  29. jac_scale/tests/test_storage.py +274 -0
  30. jac_scale/user_manager.jac +49 -0
  31. jac_scale/webhook.jac +93 -0
  32. {jac_scale-0.1.1.dist-info → jac_scale-0.1.4.dist-info}/METADATA +11 -4
  33. {jac_scale-0.1.1.dist-info → jac_scale-0.1.4.dist-info}/RECORD +36 -23
  34. {jac_scale-0.1.1.dist-info → jac_scale-0.1.4.dist-info}/WHEEL +1 -1
  35. {jac_scale-0.1.1.dist-info → jac_scale-0.1.4.dist-info}/entry_points.txt +0 -0
  36. {jac_scale-0.1.1.dist-info → jac_scale-0.1.4.dist-info}/top_level.txt +0 -0
@@ -34,7 +34,7 @@ class JacScalePluginConfig {
34
34
  "nested": {
35
35
  "secret": {
36
36
  "type": "string",
37
- "default": "supersecretkey",
37
+ "default": "supersecretkey_for_testing_only!",
38
38
  "description": "Secret key for JWT signing"
39
39
  },
40
40
  "algorithm": {
@@ -149,6 +149,33 @@ class JacScalePluginConfig {
149
149
  "type": "bool",
150
150
  "default": True,
151
151
  "description": "Enable Redis deployment in Kubernetes"
152
+ },
153
+ "plugin_versions": {
154
+ "type": "dict",
155
+ "default": {},
156
+ "description": "Package versions for PyPI installation (default mode). Use 'latest' or specific version.",
157
+ "nested": {
158
+ "jaclang": {
159
+ "type": "string",
160
+ "default": "latest",
161
+ "description": "jaclang package version"
162
+ },
163
+ "jac_scale": {
164
+ "type": "string",
165
+ "default": "latest",
166
+ "description": "jac-scale package version"
167
+ },
168
+ "jac_client": {
169
+ "type": "string",
170
+ "default": "latest",
171
+ "description": "jac-client package version"
172
+ },
173
+ "jac_byllm": {
174
+ "type": "string",
175
+ "default": "latest",
176
+ "description": "jac-byllm package version (use 'none' to skip)"
177
+ }
178
+ }
152
179
  }
153
180
  }
154
181
  },
jac_scale/serve.jac CHANGED
@@ -6,7 +6,9 @@ import from pathlib { Path }
6
6
  import from pydantic { BaseModel, Field }
7
7
  import from typing { Any }
8
8
  import jwt;
9
+ import json;
9
10
  import from os { getenv }
11
+ import from fastapi { Request }
10
12
  import from fastapi.middleware.cors { CORSMiddleware }
11
13
  import from fastapi.responses { HTMLResponse, JSONResponse, Response, RedirectResponse }
12
14
  import from jac_scale.jserver.jfast_api { JFastApiServer }
@@ -24,6 +26,10 @@ import from enum { StrEnum }
24
26
  import from fastapi_sso.sso.google { GoogleSSO }
25
27
  import from jac_scale.utils { generate_random_password }
26
28
  import from jac_scale.config_loader { get_scale_config }
29
+ import from jac_scale.webhook { ApiKeyManager, WebhookUtils }
30
+ import from typing { AsyncGenerator }
31
+ import from inspect { isgenerator }
32
+ import from fastapi.responses { StreamingResponse }
27
33
 
28
34
  # Load configuration from jac.toml with env var overrides
29
35
  glob _jwt_config = get_scale_config().get_jwt_config(),
@@ -38,14 +44,14 @@ enum Platforms ( StrEnum ) { GOOGLE = 'google' }
38
44
 
39
45
  enum Operations ( StrEnum ) { LOGIN = 'login', REGISTER = 'register' }
40
46
 
47
+ enum TransportType ( StrEnum ) { HTTP = 'http', WEBHOOK = 'webhook' }
48
+
41
49
  obj JacAPIServer(JServer) {
42
50
  # HMR (Hot Module Replacement) support fields
43
51
  has _hmr_pending: bool = False,
44
- _hot_reloader: Any | None = None;
52
+ _hot_reloader: Any | None = None,
53
+ _api_key_manager: ApiKeyManager | None = None;
45
54
 
46
- static def create_jwt_token(username: str) -> str;
47
- static def validate_jwt_token(token: str) -> (str | None);
48
- static def refresh_jwt_token(token: str) -> (str | None);
49
55
  def postinit -> None;
50
56
  def login(username: str, password: str) -> TransportResponse;
51
57
  def register_login_endpoint -> None;
@@ -66,27 +72,21 @@ obj JacAPIServer(JServer) {
66
72
  def refresh_token(token: (str | None) = None) -> TransportResponse;
67
73
  def register_create_user_endpoint -> None;
68
74
  def register_refresh_token_endpoint -> None;
69
- def get_sso(platform: str, operation: str) -> (GoogleSSO | None);
70
- async def sso_initiate(
71
- platform: str, operation: str
72
- ) -> (Response | TransportResponse);
73
-
74
- async def sso_callback(
75
- request: Request, platform: str, operation: str
76
- ) -> TransportResponse;
77
-
78
75
  def register_sso_endpoints -> None;
79
76
  def create_walker_callback(
80
77
  walker_name: str, has_node_param: bool = False
81
78
  ) -> Callable[..., TransportResponse];
82
79
 
83
80
  def create_walker_parameters(
84
- walker_name: str, invoke_on_root: bool
81
+ walker_name: str, invoke_on_root: bool, method: HTTPMethod = HTTPMethod.POST
85
82
  ) -> list[APIParameter];
86
83
 
87
84
  def register_walkers_endpoints -> None;
88
85
  def create_function_callback(func_name: str) -> Callable[..., TransportResponse];
89
- def create_function_parameters(func_name: str) -> list[APIParameter];
86
+ def create_function_parameters(
87
+ func_name: str, method: HTTPMethod = HTTPMethod.POST
88
+ ) -> list[APIParameter];
89
+
90
90
  def register_functions_endpoints -> None;
91
91
  def render_page_callback -> Callable[..., HTMLResponse];
92
92
  def render_base_route_callback(app_name: str) -> Callable[..., HTMLResponse];
@@ -98,12 +98,29 @@ obj JacAPIServer(JServer) {
98
98
  def register_root_asset_endpoint -> None;
99
99
  def serve_root_asset(file_path: str) -> Response;
100
100
  def _configure_openapi_security -> None;
101
- def start(dev: bool = False) -> None;
101
+ def start(dev: bool = False, no_client: bool = False) -> None;
102
102
  # HMR (Hot Module Replacement) dynamic routing methods
103
103
  def enable_hmr(hot_reloader: Any) -> None;
104
104
  def register_dynamic_walker_endpoint -> None;
105
105
  def register_dynamic_function_endpoint -> None;
106
106
  def register_dynamic_introspection_endpoints -> None;
107
+ # Webhook support methods
108
+ def get_api_key_manager -> ApiKeyManager;
109
+ def create_api_key(
110
+ name: str, expiry_days: int | None = None, Authorization: str | None = None
111
+ ) -> TransportResponse;
112
+
113
+ def list_api_keys(Authorization: str | None = None) -> TransportResponse;
114
+ def revoke_api_key(
115
+ api_key_id: str, Authorization: str | None = None
116
+ ) -> TransportResponse;
117
+
118
+ def register_api_key_endpoints -> None;
119
+ def register_webhook_endpoints -> None;
120
+ def create_webhook_callback(walker_name: str) -> Callable[..., TransportResponse];
121
+ def create_webhook_parameters(walker_name: str) -> list[APIParameter];
122
+ def register_dynamic_webhook_endpoint -> None;
123
+ def get_transport_type_for_walker(walker_name: str) -> str;
107
124
  }
108
125
 
109
126
  class UpdateUsernameRequest(BaseModel) {
@@ -0,0 +1,72 @@
1
+ """SSO Provider Abstraction for jac-scale.
2
+
3
+ This module defines the abstract interface for SSO providers, enabling
4
+ easy addition of new authentication vendors (Google, Microsoft, GitHub, SAML, etc.).
5
+ """
6
+
7
+ import from typing { Any }
8
+ import from fastapi { Request, Response }
9
+ import from dataclasses { dataclass }
10
+
11
+ """Standardized user information from SSO providers."""
12
+ @dataclass
13
+ class SSOUserInfo {
14
+ has email: str,
15
+ external_id: str,
16
+ platform: str,
17
+ display_name: (str | None) = None;
18
+ }
19
+
20
+ """Abstract base class for SSO providers.
21
+
22
+ All SSO provider implementations must inherit from this class and implement
23
+ the required methods. This abstraction enables:
24
+
25
+ 1. Consistent interface across all SSO vendors
26
+ 2. Easy addition of new providers (just implement this interface)
27
+ 3. Testability through mock implementations
28
+ 4. Standardized user information format
29
+
30
+ Example:
31
+ To add a new SSO provider (e.g., Microsoft):
32
+
33
+ ```jac
34
+ obj MicrosoftSSOProvider(SSOProvider) {
35
+ # Implement the three required methods
36
+ async def initiate_auth(operation: str) -> Response;
37
+ async def handle_callback(request: Request) -> SSOUserInfo;
38
+ def get_platform_name() -> str;
39
+ }
40
+ ```
41
+ """
42
+ obj SSOProvider {
43
+ """Initialize SSO authentication flow.
44
+
45
+ Args:
46
+ operation: The operation type ('login' or 'register')
47
+
48
+ Returns:
49
+ Response object (typically a redirect to the SSO provider's auth page)
50
+ """
51
+ async def initiate_auth(operation: str) -> Response;
52
+
53
+ """Handle the OAuth callback from the SSO provider.
54
+
55
+ Args:
56
+ request: The FastAPI request object containing the OAuth callback data
57
+
58
+ Returns:
59
+ SSOUserInfo: Standardized user information from the provider
60
+
61
+ Raises:
62
+ Exception: If authentication fails or user info cannot be retrieved
63
+ """
64
+ async def handle_callback(request: Request) -> SSOUserInfo;
65
+
66
+ """Get the platform identifier for this provider.
67
+
68
+ Returns:
69
+ str: Platform name (e.g., 'google', 'microsoft', 'github')
70
+ """
71
+ def get_platform_name -> str;
72
+ }
@@ -34,12 +34,12 @@ class KubernetesConfig(BaseConfig) {
34
34
  app_mount_path: str = '/app',
35
35
  code_mount_path: str = '/code',
36
36
  workspace_path: str = '/code/workspace',
37
- # Runtime environment (defaults to official Jaseci repo)
38
37
  jaseci_repo_url: str = 'https://github.com/jaseci-labs/jaseci.git',
39
38
  jaseci_branch: str = 'main',
40
39
  jaseci_commit: (str | None) = None,
41
40
  install_jaseci: bool = True,
42
- additional_packages: list[str] = [];
41
+ additional_packages: list[str] = [],
42
+ plugin_versions: dict[str, str] = {};
43
43
 
44
44
  def init(
45
45
  self: KubernetesConfig,
@@ -75,12 +75,8 @@ class KubernetesConfig(BaseConfig) {
75
75
  jaseci_branch: str = 'main',
76
76
  jaseci_commit: (str | None) = None,
77
77
  install_jaseci: bool = True,
78
- additional_packages: list[str] = []
79
- # Runtime configuration
80
- # Storage configuration
81
- # Timing configuration
82
- # Paths
83
- # Runtime environment
78
+ additional_packages: list[str] = [],
79
+ plugin_versions: dict[str, str] = {}
84
80
  ) -> None {
85
81
  self.app_name = app_name;
86
82
  self.namespace = namespace;
@@ -121,6 +117,7 @@ class KubernetesConfig(BaseConfig) {
121
117
  self.jaseci_commit = jaseci_commit;
122
118
  self.install_jaseci = install_jaseci;
123
119
  self.additional_packages = additional_packages;
120
+ self.plugin_versions = plugin_versions;
124
121
  }
125
122
 
126
123
  override def to_dict(self: KubernetesConfig) -> dict[str, Any] {
@@ -162,7 +159,8 @@ class KubernetesConfig(BaseConfig) {
162
159
  'jaseci_branch': self.jaseci_branch,
163
160
  'jaseci_commit': self.jaseci_commit,
164
161
  'install_jaseci': self.install_jaseci,
165
- 'additional_packages': self.additional_packages
162
+ 'additional_packages': self.additional_packages,
163
+ 'plugin_versions': self.plugin_versions
166
164
  }
167
165
  );
168
166
  return base;
@@ -204,12 +202,8 @@ class KubernetesConfig(BaseConfig) {
204
202
  jaseci_branch=config.get('jaseci_branch', 'main'),
205
203
  jaseci_commit=config.get('jaseci_commit'),
206
204
  install_jaseci=config.get('install_jaseci', True),
207
- additional_packages=config.get('additional_packages', [])
208
- # Runtime configuration
209
- # Storage configuration
210
- # Timing configuration
211
- # Paths
212
- # Runtime environment
205
+ additional_packages=config.get('additional_packages', []),
206
+ plugin_versions=config.get('plugin_versions', {})
213
207
  );
214
208
  }
215
209
  }
@@ -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', 'npm', 'nodejs'];
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
- # Clone and setup Jaseci if enabled
199
- if config.install_jaseci {
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
+ }