jac-scale 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 (57) hide show
  1. jac_scale/__init__.py +0 -0
  2. jac_scale/abstractions/config/app_config.jac +30 -0
  3. jac_scale/abstractions/config/base_config.jac +26 -0
  4. jac_scale/abstractions/database_provider.jac +51 -0
  5. jac_scale/abstractions/deployment_target.jac +64 -0
  6. jac_scale/abstractions/image_registry.jac +54 -0
  7. jac_scale/abstractions/logger.jac +20 -0
  8. jac_scale/abstractions/models/deployment_result.jac +27 -0
  9. jac_scale/abstractions/models/resource_status.jac +38 -0
  10. jac_scale/config_loader.jac +31 -0
  11. jac_scale/context.jac +14 -0
  12. jac_scale/factories/database_factory.jac +43 -0
  13. jac_scale/factories/deployment_factory.jac +43 -0
  14. jac_scale/factories/registry_factory.jac +32 -0
  15. jac_scale/factories/utility_factory.jac +34 -0
  16. jac_scale/impl/config_loader.impl.jac +131 -0
  17. jac_scale/impl/context.impl.jac +24 -0
  18. jac_scale/impl/memory_hierarchy.main.impl.jac +63 -0
  19. jac_scale/impl/memory_hierarchy.mongo.impl.jac +239 -0
  20. jac_scale/impl/memory_hierarchy.redis.impl.jac +186 -0
  21. jac_scale/impl/serve.impl.jac +1785 -0
  22. jac_scale/jserver/__init__.py +0 -0
  23. jac_scale/jserver/impl/jfast_api.impl.jac +731 -0
  24. jac_scale/jserver/impl/jserver.impl.jac +79 -0
  25. jac_scale/jserver/jfast_api.jac +162 -0
  26. jac_scale/jserver/jserver.jac +101 -0
  27. jac_scale/memory_hierarchy.jac +138 -0
  28. jac_scale/plugin.jac +218 -0
  29. jac_scale/plugin_config.jac +175 -0
  30. jac_scale/providers/database/kubernetes_mongo.jac +137 -0
  31. jac_scale/providers/database/kubernetes_redis.jac +110 -0
  32. jac_scale/providers/registry/dockerhub.jac +64 -0
  33. jac_scale/serve.jac +118 -0
  34. jac_scale/targets/kubernetes/kubernetes_config.jac +215 -0
  35. jac_scale/targets/kubernetes/kubernetes_target.jac +841 -0
  36. jac_scale/targets/kubernetes/utils/kubernetes_utils.impl.jac +519 -0
  37. jac_scale/targets/kubernetes/utils/kubernetes_utils.jac +85 -0
  38. jac_scale/tests/__init__.py +0 -0
  39. jac_scale/tests/conftest.py +29 -0
  40. jac_scale/tests/fixtures/test_api.jac +159 -0
  41. jac_scale/tests/fixtures/todo_app.jac +68 -0
  42. jac_scale/tests/test_abstractions.py +88 -0
  43. jac_scale/tests/test_deploy_k8s.py +265 -0
  44. jac_scale/tests/test_examples.py +484 -0
  45. jac_scale/tests/test_factories.py +149 -0
  46. jac_scale/tests/test_file_upload.py +444 -0
  47. jac_scale/tests/test_k8s_utils.py +156 -0
  48. jac_scale/tests/test_memory_hierarchy.py +247 -0
  49. jac_scale/tests/test_serve.py +1835 -0
  50. jac_scale/tests/test_sso.py +711 -0
  51. jac_scale/utilities/loggers/standard_logger.jac +40 -0
  52. jac_scale/utils.jac +16 -0
  53. jac_scale-0.1.1.dist-info/METADATA +658 -0
  54. jac_scale-0.1.1.dist-info/RECORD +57 -0
  55. jac_scale-0.1.1.dist-info/WHEEL +5 -0
  56. jac_scale-0.1.1.dist-info/entry_points.txt +3 -0
  57. jac_scale-0.1.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,159 @@
1
+ """Test API fixture for jac-scale serve tests."""
2
+
3
+ # ============================================================================
4
+ # Test Functions
5
+ # ============================================================================
6
+ def add_numbers(
7
+ a: int, b: int
8
+ ) -> int {
9
+ return a + b;
10
+ }
11
+
12
+ def greet(name: str = "World") -> str {
13
+ return "Hello, " + name + "!";
14
+ }
15
+
16
+ def multiply(x: int, y: int) -> int {
17
+ return x * y;
18
+ }
19
+
20
+ # ============================================================================
21
+ # Test Data Models
22
+ # ============================================================================
23
+ node Task {
24
+ has title: str;
25
+ has priority: int = 1;
26
+ has completed: bool = False;
27
+ }
28
+
29
+ node TaskList {
30
+ has name: str = "My Tasks";
31
+ }
32
+
33
+ # ============================================================================
34
+ # Test Walkers
35
+ # ============================================================================
36
+ walker CreateTask {
37
+ has title: str;
38
+ has priority: int = 1;
39
+
40
+ can create with `root entry {
41
+ new_task = here ++> Task(title=self.title, priority=self.priority);
42
+ report new_task ;
43
+ }
44
+ }
45
+
46
+ walker ListTasks {
47
+ can list with `root entry {
48
+ tasks = [-->];
49
+ report tasks ;
50
+ }
51
+ }
52
+
53
+ walker CompleteTask {
54
+ has title: str;
55
+
56
+ can complete with `root entry {
57
+ for task in [-->] {
58
+ if task.title == self.title {
59
+ visit task;
60
+ }
61
+ }
62
+ }
63
+
64
+ can mark_done with Task entry {
65
+ here.completed = True;
66
+ report here ;
67
+ }
68
+ }
69
+
70
+ walker GetTask {
71
+ can get with Task entry {
72
+ report here ;
73
+ }
74
+ }
75
+
76
+ walker : priv PrivateCreateTask {
77
+ has title: str;
78
+ has priority: int = 1;
79
+
80
+ can create with `root entry {
81
+ new_task = here ++> Task(title=self.title, priority=self.priority);
82
+ report {"message": "Private task created", "task": new_task} ;
83
+ }
84
+ }
85
+
86
+ walker : pub PublicInfo {
87
+ can get_info with `root entry {
88
+ report {"message": "This is a public endpoint", "auth_required": False} ;
89
+ }
90
+ }
91
+
92
+ # Async Walker Test
93
+ import asyncio;
94
+
95
+ """Async walker that creates a task with simulated async delay."""
96
+ async walker AsyncCreateTask {
97
+ has title: str;
98
+ has delay_ms: int = 100;
99
+
100
+ async can create with `root entry {
101
+ report {"status": "started", "title": self.title};
102
+ # Simulate async operation (e.g., external API call, DB operation)
103
+ delay_seconds = float(self.delay_ms) / 1000.0;
104
+ await asyncio.sleep(delay_seconds);
105
+ report {"status": "after_async_wait"};
106
+ new_task = here ++> Task(title=self.title, priority=1);
107
+ report {"status": "completed", "task": new_task};
108
+ }
109
+ }
110
+
111
+ # ============================================================================
112
+ # File Upload Walkers
113
+ # ============================================================================
114
+ import from fastapi { UploadFile }
115
+
116
+ """Walker that handles single file upload."""
117
+ walker UploadSingleFile {
118
+ has myfile: UploadFile;
119
+ has description: str = "";
120
+
121
+ can process with `root entry {
122
+ report {
123
+ "filename": self.myfile.filename,
124
+ "content_type": self.myfile.content_type,
125
+ "size": self.myfile.size,
126
+ "description": self.description
127
+ };
128
+ }
129
+ }
130
+
131
+ """Walker with file upload that requires authentication (private by default)."""
132
+ walker SecureFileUpload {
133
+ has document: UploadFile;
134
+ has notes: str = "";
135
+
136
+ can process with `root entry {
137
+ report {
138
+ "filename": self.document.filename,
139
+ "content_type": self.document.content_type,
140
+ "size": self.document.size,
141
+ "notes": self.notes,
142
+ "authenticated": True
143
+ };
144
+ }
145
+ }
146
+
147
+ """Public walker with file upload (no auth required)."""
148
+ walker : pub PublicFileUpload {
149
+ has attachment: UploadFile;
150
+
151
+ can process with `root entry {
152
+ report {
153
+ "filename": self.attachment.filename,
154
+ "content_type": self.attachment.content_type,
155
+ "size": self.attachment.size,
156
+ "public": True
157
+ };
158
+ }
159
+ }
@@ -0,0 +1,68 @@
1
+ node Task {
2
+ has id: int,
3
+ title: str,
4
+ completed: bool = False;
5
+ }
6
+
7
+ walker CreateTask {
8
+ has id: int,
9
+ title: str;
10
+
11
+ can create with `root entry {
12
+ new_task = here ++> Task(id=self.id, title=self.title);
13
+ report new_task ;
14
+ }
15
+ }
16
+
17
+ walker GetAllTasks {
18
+ can read with `root entry{
19
+ tasks = [-->];
20
+ report tasks;
21
+ }
22
+ }
23
+
24
+ walker GetTask {
25
+ has id: int;
26
+
27
+ can get with `root entry{
28
+ for task in [-->]{
29
+ if (self.id == task.id){
30
+ report task;
31
+ }
32
+ }
33
+ }
34
+ }
35
+
36
+ walker CompleteTask {
37
+ has id: int;
38
+
39
+ can complete with `root entry {
40
+ for task in [-->Task] {
41
+ if (self.id == task.id){
42
+ visit task;
43
+ }
44
+ }
45
+ }
46
+
47
+ can mark_complete with Task entry {
48
+ here.completed = True;
49
+ report here ; # here is the visited task
50
+ }
51
+ }
52
+
53
+ walker DeleteTask {
54
+ has id: int;
55
+
56
+ can visit_node with `root entry {
57
+ for task in [-->] {
58
+ if (self.id == task.id){
59
+ visit task;
60
+ }
61
+ }
62
+ }
63
+
64
+ can delete with `root entry {
65
+ del here;
66
+ report {"deleted": True, "id": self.id};
67
+ }
68
+ }
@@ -0,0 +1,88 @@
1
+ """Tests for abstraction base classes."""
2
+
3
+ import pytest
4
+
5
+ # Note: These imports will work once the Jac files are compiled to Python
6
+ try:
7
+ from ..abstractions.config.app_config import AppConfig
8
+ from ..abstractions.models.deployment_result import DeploymentResult
9
+ from ..abstractions.models.resource_status import (
10
+ ResourceStatus,
11
+ ResourceStatusInfo,
12
+ )
13
+ except ImportError:
14
+ # If Jac files aren't compiled yet, skip these tests
15
+ pytest.skip("Jac modules not compiled", allow_module_level=True)
16
+
17
+
18
+ def test_deployment_result_success():
19
+ """Test DeploymentResult with success."""
20
+ result = DeploymentResult(
21
+ success=True,
22
+ service_url="http://localhost:8000",
23
+ message="Deployment successful",
24
+ )
25
+
26
+ assert result.success is True
27
+ assert result.is_successful() is True
28
+ assert result.service_url == "http://localhost:8000"
29
+ assert result.message == "Deployment successful"
30
+
31
+
32
+ def test_deployment_result_failure():
33
+ """Test DeploymentResult with failure."""
34
+ result = DeploymentResult(
35
+ success=False,
36
+ message="Deployment failed",
37
+ details={"error": "Connection timeout"},
38
+ )
39
+
40
+ assert result.success is False
41
+ assert result.is_successful() is False
42
+ assert result.message == "Deployment failed"
43
+ assert "error" in result.details
44
+
45
+
46
+ def test_resource_status_info_ready():
47
+ """Test ResourceStatusInfo when resource is ready."""
48
+ status = ResourceStatusInfo(
49
+ status=ResourceStatus.RUNNING,
50
+ replicas=3,
51
+ ready_replicas=3,
52
+ )
53
+
54
+ assert status.is_ready() is True
55
+ assert status.replicas == 3
56
+ assert status.ready_replicas == 3
57
+
58
+
59
+ def test_resource_status_info_not_ready():
60
+ """Test ResourceStatusInfo when resource is not ready."""
61
+ status = ResourceStatusInfo(
62
+ status=ResourceStatus.PENDING,
63
+ replicas=3,
64
+ ready_replicas=1,
65
+ )
66
+
67
+ assert status.is_ready() is False
68
+ assert status.replicas == 3
69
+ assert status.ready_replicas == 1
70
+
71
+
72
+ def test_resource_status_enum():
73
+ """Test ResourceStatus enum values."""
74
+ assert ResourceStatus.UNKNOWN == "unknown"
75
+ assert ResourceStatus.PENDING == "pending"
76
+ assert ResourceStatus.RUNNING == "running"
77
+ assert ResourceStatus.FAILED == "failed"
78
+ assert ResourceStatus.STOPPED == "stopped"
79
+
80
+
81
+ def test_app_config_get_code_path():
82
+ """Test AppConfig.get_code_path() method."""
83
+ app_config = AppConfig(code_folder="/path/to/code")
84
+
85
+ path = app_config.get_code_path()
86
+
87
+ assert str(path) == "/path/to/code"
88
+ assert hasattr(path, "exists") # Should be a Path object
@@ -0,0 +1,265 @@
1
+ """Tests for Kubernetes deployment using new factory-based architecture."""
2
+
3
+ import os
4
+ import time
5
+ from typing import Any
6
+
7
+ import requests
8
+ from kubernetes import client, config
9
+ from kubernetes.client.exceptions import ApiException
10
+
11
+ from ..abstractions.config.app_config import AppConfig
12
+ from ..config_loader import get_scale_config
13
+ from ..factories.deployment_factory import DeploymentTargetFactory
14
+ from ..factories.utility_factory import UtilityFactory
15
+
16
+
17
+ def _request_with_retry(
18
+ method: str,
19
+ url: str,
20
+ json: dict[str, Any] | None = None,
21
+ timeout: int = 10,
22
+ max_retries: int = 60,
23
+ retry_interval: float = 2.0,
24
+ ) -> requests.Response:
25
+ """Make an HTTP request with retry logic for 503 responses.
26
+
27
+ Args:
28
+ method: HTTP method (GET, POST, etc.)
29
+ url: The URL to request
30
+ json: JSON payload for the request
31
+ timeout: Request timeout in seconds
32
+ max_retries: Maximum number of retries for 503 responses
33
+ retry_interval: Time to wait between retries in seconds
34
+
35
+ Returns:
36
+ Response object
37
+ """
38
+ response = None
39
+ for attempt in range(max_retries):
40
+ response = requests.request(
41
+ method=method,
42
+ url=url,
43
+ json=json,
44
+ timeout=timeout,
45
+ )
46
+
47
+ if response.status_code == 503:
48
+ print(
49
+ f"[DEBUG] {url} returned 503, retrying ({attempt + 1}/{max_retries})..."
50
+ )
51
+ time.sleep(retry_interval)
52
+ continue
53
+
54
+ return response
55
+
56
+ # Return last response even if it was 503
57
+ assert response is not None, "No response received"
58
+ return response
59
+
60
+
61
+ def test_deploy_all_in_one():
62
+ """
63
+ Test deployment using the new factory-based architecture.
64
+ Deploys the all-in-one app found in jac client examples against a live Kubernetes cluster.
65
+ Validates deployment, services, sends HTTP request, and tests cleanup.
66
+ """
67
+
68
+ # Load kubeconfig and initialize client
69
+ config.load_kube_config()
70
+ apps_v1 = client.AppsV1Api()
71
+ core_v1 = client.CoreV1Api()
72
+
73
+ namespace = "all-in-one"
74
+ app_name = namespace
75
+
76
+ # Set environment
77
+ os.environ.update({"APP_NAME": app_name, "K8s_NAMESPACE": namespace})
78
+
79
+ # Resolve the absolute path to the todo app folder
80
+ test_dir = os.path.dirname(os.path.abspath(__file__))
81
+ todo_app_path = os.path.join(
82
+ test_dir, "../../../jac-client/jac_client/examples/all-in-one"
83
+ )
84
+
85
+ # Get configuration
86
+ scale_config = get_scale_config()
87
+ target_config = scale_config.get_kubernetes_config()
88
+ target_config["app_name"] = app_name
89
+ target_config["namespace"] = namespace
90
+
91
+ # Create logger
92
+ logger = UtilityFactory.create_logger("standard")
93
+
94
+ # Create deployment target using factory
95
+ deployment_target = DeploymentTargetFactory.create(
96
+ "kubernetes", target_config, logger
97
+ )
98
+
99
+ # Create app config
100
+ app_config = AppConfig(
101
+ code_folder=todo_app_path,
102
+ file_name="main.jac",
103
+ build=False,
104
+ )
105
+
106
+ # Deploy using new architecture
107
+ result = deployment_target.deploy(app_config)
108
+
109
+ assert result.success is True
110
+ print(f"✓ Deployment successful: {result.message}")
111
+
112
+ # Wait a moment for services to stabilize
113
+ time.sleep(5)
114
+
115
+ # Validate the main deployment exists
116
+ deployment = apps_v1.read_namespaced_deployment(name=app_name, namespace=namespace)
117
+ assert deployment.metadata.name == app_name
118
+ assert deployment.spec.replicas == 1
119
+
120
+ # Validate main service
121
+ service = core_v1.read_namespaced_service(
122
+ name=f"{app_name}-service", namespace=namespace
123
+ )
124
+ assert service.spec.type == "NodePort"
125
+ node_port = service.spec.ports[0].node_port
126
+ print(f"✓ Service is exposed on NodePort: {node_port}")
127
+
128
+ # Validate MongoDB StatefulSet and Service
129
+ mongodb_stateful = apps_v1.read_namespaced_stateful_set(
130
+ name=f"{app_name}-mongodb", namespace=namespace
131
+ )
132
+ assert mongodb_stateful.metadata.name == f"{app_name}-mongodb"
133
+ assert mongodb_stateful.spec.service_name == f"{app_name}-mongodb-service"
134
+
135
+ mongodb_service = core_v1.read_namespaced_service(
136
+ name=f"{app_name}-mongodb-service", namespace=namespace
137
+ )
138
+ assert mongodb_service.spec.ports[0].port == 27017
139
+
140
+ # Validate Redis Deployment and Service
141
+ redis_deploy = apps_v1.read_namespaced_deployment(
142
+ name=f"{app_name}-redis", namespace=namespace
143
+ )
144
+ assert redis_deploy.metadata.name == f"{app_name}-redis"
145
+
146
+ redis_service = core_v1.read_namespaced_service(
147
+ name=f"{app_name}-redis-service", namespace=namespace
148
+ )
149
+ assert redis_service.spec.ports[0].port == 6379
150
+
151
+ # Test get_status
152
+ status = deployment_target.get_status(app_name)
153
+ assert status is not None
154
+ assert status.replicas >= 0
155
+ print(
156
+ f"✓ Deployment status: {status.status.value}, replicas: {status.replicas}/{status.ready_replicas}"
157
+ )
158
+
159
+ # Send POST request to create a todo (with retry for 503)
160
+ url = f"http://localhost:{node_port}/walker/create_todo"
161
+ payload = {"text": "first-task"}
162
+ response = _request_with_retry("POST", url, json=payload, timeout=10)
163
+ assert response.status_code == 200
164
+ print(f"✓ Successfully created todo at {url}")
165
+ print(f" Response: {response.json()}")
166
+
167
+ url = f"http://localhost:{node_port}/cl/app"
168
+ response = _request_with_retry("GET", url, timeout=100)
169
+ print(f"Response status code for app page: {response.status_code}")
170
+ assert response.status_code == 200
171
+ print(f"✓ Successfully reached app page at {url}")
172
+
173
+ # Cleanup using new architecture
174
+ deployment_target.destroy(app_name)
175
+ time.sleep(60) # Wait for deletion to propagate
176
+
177
+ # Verify cleanup - resources should no longer exist
178
+ try:
179
+ apps_v1.read_namespaced_deployment(app_name, namespace=namespace)
180
+ raise AssertionError("Deployment should have been deleted")
181
+ except ApiException as e:
182
+ assert e.status == 404, f"Expected 404, got {e.status}"
183
+
184
+ try:
185
+ core_v1.read_namespaced_service(f"{app_name}-service", namespace=namespace)
186
+ raise AssertionError("Service should have been deleted")
187
+ except ApiException as e:
188
+ assert e.status == 404, f"Expected 404, got {e.status}"
189
+
190
+ try:
191
+ apps_v1.read_namespaced_stateful_set(f"{app_name}-mongodb", namespace=namespace)
192
+ raise AssertionError("MongoDB StatefulSet should have been deleted")
193
+ except ApiException as e:
194
+ assert e.status == 404, f"Expected 404, got {e.status}"
195
+
196
+ try:
197
+ core_v1.read_namespaced_service(
198
+ f"{app_name}-mongodb-service", namespace=namespace
199
+ )
200
+ raise AssertionError("MongoDB Service should have been deleted")
201
+ except ApiException as e:
202
+ assert e.status == 404, f"Expected 404, got {e.status}"
203
+
204
+ try:
205
+ apps_v1.read_namespaced_deployment(f"{app_name}-redis", namespace=namespace)
206
+ raise AssertionError("Redis Deployment should have been deleted")
207
+ except ApiException as e:
208
+ assert e.status == 404, f"Expected 404, got {e.status}"
209
+
210
+ try:
211
+ core_v1.read_namespaced_service(
212
+ f"{app_name}-redis-service", namespace=namespace
213
+ )
214
+ raise AssertionError("Redis Service should have been deleted")
215
+ except ApiException as e:
216
+ assert e.status == 404, f"Expected 404, got {e.status}"
217
+
218
+ # Verify PVC cleanup
219
+ pvcs = core_v1.list_namespaced_persistent_volume_claim(namespace=namespace)
220
+ for pvc in pvcs.items:
221
+ assert not pvc.metadata.name.startswith(app_name), (
222
+ f"PVC '{pvc.metadata.name}' should have been deleted"
223
+ )
224
+
225
+ print("✓ Cleanup verification complete - all resources properly deleted")
226
+
227
+
228
+ def test_deployment_target_methods():
229
+ """Test individual methods of KubernetesTarget."""
230
+ # Load kubeconfig
231
+ config.load_kube_config()
232
+
233
+ namespace = "test-methods"
234
+ app_name = "test-methods-app"
235
+
236
+ # Set environment
237
+ os.environ.update({"APP_NAME": app_name, "K8s_NAMESPACE": namespace})
238
+
239
+ # Get configuration
240
+ scale_config = get_scale_config()
241
+ target_config = scale_config.get_kubernetes_config()
242
+ target_config["app_name"] = app_name
243
+ target_config["namespace"] = namespace
244
+
245
+ # Create deployment target
246
+ logger = UtilityFactory.create_logger("standard")
247
+ deployment_target = DeploymentTargetFactory.create(
248
+ "kubernetes", target_config, logger
249
+ )
250
+
251
+ # Test get_service_url (before deployment, should return None or handle gracefully)
252
+ service_url = deployment_target.get_service_url(app_name)
253
+ # Service URL may be None if service doesn't exist yet
254
+ assert service_url is None or isinstance(service_url, str)
255
+
256
+ # Test get_status (before deployment, should handle gracefully)
257
+ try:
258
+ status = deployment_target.get_status(app_name)
259
+ # Should return UNKNOWN or handle the error gracefully
260
+ assert status is not None
261
+ except Exception:
262
+ # It's okay if it raises an exception for non-existent deployment
263
+ pass
264
+
265
+ print("✓ Deployment target methods tested")