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,247 @@
1
+ import contextlib
2
+ import gc
3
+ import os
4
+ import socket
5
+ import subprocess
6
+ import sys
7
+ import time
8
+ from pathlib import Path
9
+ from typing import Any, cast
10
+
11
+ import redis
12
+ import requests
13
+ from pymongo import MongoClient
14
+ from testcontainers.mongodb import MongoDbContainer
15
+ from testcontainers.redis import RedisContainer
16
+
17
+
18
+ def get_free_port() -> int:
19
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
20
+ s.bind(("", 0))
21
+ return s.getsockname()[1]
22
+
23
+
24
+ class TestMemoryHierarchy:
25
+ fixtures_dir: Path
26
+ jac_file: Path
27
+ base_url: str
28
+ port: int
29
+
30
+ redis_client: redis.Redis
31
+ mongo_client: MongoClient
32
+
33
+ redis_container: RedisContainer
34
+ mongo_container: MongoDbContainer
35
+ server: subprocess.Popen[str] | None = None
36
+
37
+ @classmethod
38
+ def setup_class(cls) -> None:
39
+ cls.fixtures_dir = Path(__file__).parent / "fixtures"
40
+ cls.jac_file = cls.fixtures_dir / "todo_app.jac"
41
+
42
+ if not cls.jac_file.exists():
43
+ raise FileNotFoundError(f"Missing Jac file: {cls.jac_file}")
44
+
45
+ # Clean up session file from previous runs to ensure test isolation
46
+ session_file = (
47
+ cls.fixtures_dir / ".jac" / "data" / "todo_app.session.users.json"
48
+ )
49
+ if session_file.exists():
50
+ os.remove(session_file)
51
+
52
+ # start redis container
53
+ cls.redis_container = RedisContainer("redis:latest", port=6379)
54
+ cls.redis_container.start()
55
+
56
+ redis_host = cls.redis_container.get_container_host_ip()
57
+ redis_port = cls.redis_container.get_exposed_port(6379)
58
+
59
+ redis_url = f"redis://{redis_host}:{redis_port}/0"
60
+
61
+ cls.redis_client = redis.Redis(
62
+ host=redis_host, port=int(redis_port), decode_responses=False
63
+ )
64
+
65
+ # here we are verifying that redis is empty before starting tests
66
+ assert cls.redis_client.dbsize() == 0
67
+
68
+ # start mongo container
69
+ cls.mongo_container = MongoDbContainer("mongo:latest")
70
+ cls.mongo_container.start()
71
+
72
+ mongo_uri = cls.mongo_container.get_connection_url()
73
+ cls.mongo_client = MongoClient(mongo_uri)
74
+
75
+ os.environ["MONGODB_URI"] = mongo_uri
76
+ os.environ["REDIS_URL"] = redis_url
77
+
78
+ assert "jac_db" not in cls.mongo_client.list_database_names()
79
+
80
+ # setting up
81
+ cls.port = get_free_port()
82
+ cls.base_url = f"http://localhost:{cls.port}"
83
+
84
+ cls._start_server()
85
+
86
+ @classmethod
87
+ def teardown_class(cls) -> None:
88
+ if cls.server:
89
+ cls.server.terminate()
90
+ with contextlib.suppress(Exception):
91
+ cls.server.wait(timeout=5)
92
+
93
+ system_dbs = {"admin", "config", "local"}
94
+ for db_name in cls.mongo_client.list_database_names():
95
+ if db_name not in system_dbs:
96
+ cls.mongo_client.drop_database(db_name)
97
+
98
+ cls.mongo_container.stop()
99
+ cls.redis_container.stop()
100
+
101
+ time.sleep(0.5)
102
+ gc.collect()
103
+ # Clean up session file from .jac/data directory
104
+ session_file = (
105
+ cls.fixtures_dir / ".jac" / "data" / "todo_app.session.users.json"
106
+ )
107
+ if session_file.exists():
108
+ os.remove(session_file)
109
+
110
+ @classmethod
111
+ def _start_server(cls) -> None:
112
+ # Get the jac executable from the same directory as the current Python interpreter
113
+ jac_executable = Path(sys.executable).parent / "jac"
114
+ cmd = [
115
+ str(jac_executable),
116
+ "start",
117
+ str(cls.jac_file.name),
118
+ "--port",
119
+ str(cls.port),
120
+ ]
121
+
122
+ env = os.environ.copy()
123
+
124
+ cls.server = subprocess.Popen(
125
+ cmd,
126
+ stdout=subprocess.PIPE,
127
+ stderr=subprocess.PIPE,
128
+ text=True,
129
+ cwd=str(cls.fixtures_dir),
130
+ env=env,
131
+ )
132
+
133
+ for _ in range(30):
134
+ try:
135
+ r = requests.get(f"{cls.base_url}/docs", timeout=1)
136
+ if r.status_code in (200, 404):
137
+ return
138
+ except Exception:
139
+ time.sleep(1)
140
+
141
+ stdout, stderr = cls.server.communicate(timeout=2)
142
+ raise RuntimeError(
143
+ f"jac start failed to start\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}"
144
+ )
145
+
146
+ @staticmethod
147
+ def _extract_transport_response_data(
148
+ json_response: dict[str, Any] | list[Any],
149
+ ) -> dict[str, Any] | list[Any]:
150
+ """Extract data from TransportResponse envelope format."""
151
+ # Handle jac-scale's tuple response format [status, body]
152
+ if isinstance(json_response, list) and len(json_response) == 2:
153
+ body: dict[str, Any] = json_response[1]
154
+ json_response = body
155
+
156
+ # Handle TransportResponse envelope format
157
+ if (
158
+ isinstance(json_response, dict)
159
+ and "ok" in json_response
160
+ and "data" in json_response
161
+ ):
162
+ if json_response.get("ok") and json_response.get("data") is not None:
163
+ # Success case: return the data field
164
+ return json_response["data"]
165
+ elif not json_response.get("ok") and json_response.get("error"):
166
+ # Error case: return error info
167
+ error_info = json_response["error"]
168
+ result: dict[str, Any] = {
169
+ "error": error_info.get("message", "Unknown error")
170
+ }
171
+ if "code" in error_info:
172
+ result["error_code"] = error_info["code"]
173
+ if "details" in error_info:
174
+ result["error_details"] = error_info["details"]
175
+ return result
176
+ # FastAPI validation errors (422) have "detail" field - return as-is
177
+ return json_response
178
+
179
+ def _register(self, username: str, password: str = "password123") -> str:
180
+ res = requests.post(
181
+ f"{self.base_url}/user/register",
182
+ json={"username": username, "password": password},
183
+ timeout=5,
184
+ )
185
+ assert res.status_code == 201, (
186
+ f"Registration failed: {res.status_code} - {res.text}"
187
+ )
188
+ data = cast(dict[str, Any], self._extract_transport_response_data(res.json()))
189
+ return data["token"]
190
+
191
+ def _post(self, path: str, payload: dict, token: str) -> dict[str, Any]:
192
+ res = requests.post(
193
+ f"{self.base_url}{path}",
194
+ json=payload,
195
+ headers={"Authorization": f"Bearer {token}"},
196
+ timeout=5,
197
+ )
198
+ assert res.status_code == 200
199
+ return cast(dict[str, Any], self._extract_transport_response_data(res.json()))
200
+
201
+ # TODO: delete method in jac start is not working as expected. will be fixed in a separate PR and a test case will be added
202
+
203
+ def test_read_and_write(self) -> None:
204
+ db = self.mongo_client["jac_db"]
205
+ collection = db["anchors"]
206
+
207
+ mongo_doc_initial_count = collection.count_documents({})
208
+ assert (
209
+ mongo_doc_initial_count == 2
210
+ ) # the initial docs is two, because super root, guest_user
211
+
212
+ # Register a user
213
+ token = self._register("reader", "pass123")
214
+
215
+ mongo_doc_after_user_creation_count = collection.count_documents({})
216
+ assert (
217
+ mongo_doc_after_user_creation_count == 3
218
+ ) # the initial docs is three, because super root, guest_user and the created user
219
+
220
+ redis_size_before_task_creation = self.redis_client.dbsize()
221
+
222
+ # Create tasks
223
+ created_tasks = [
224
+ {"id": 203, "title": "Task 203"},
225
+ {"id": 204, "title": "Task 204"},
226
+ ]
227
+
228
+ redis_size_after_task_creation = self.redis_client.dbsize()
229
+
230
+ for task_payload in created_tasks:
231
+ self._post("/walker/CreateTask", task_payload, token)
232
+
233
+ mongo_doc_count_after_task_creation = collection.count_documents({})
234
+
235
+ assert (
236
+ mongo_doc_count_after_task_creation == 7
237
+ ) # the previous 3 and two anchors (1 node + 1 edge) for each task
238
+
239
+ assert redis_size_after_task_creation == redis_size_before_task_creation
240
+
241
+ self._post("/walker/GetAllTasks", {}, token)
242
+
243
+ redis_size_after_task_read = self.redis_client.dbsize()
244
+
245
+ assert (
246
+ redis_size_after_task_read == 7
247
+ ) # super root, guest user, created user, two task nodes, and two edges