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.
- jac_scale/__init__.py +0 -0
- jac_scale/abstractions/config/app_config.jac +30 -0
- jac_scale/abstractions/config/base_config.jac +26 -0
- jac_scale/abstractions/database_provider.jac +51 -0
- jac_scale/abstractions/deployment_target.jac +64 -0
- jac_scale/abstractions/image_registry.jac +54 -0
- jac_scale/abstractions/logger.jac +20 -0
- jac_scale/abstractions/models/deployment_result.jac +27 -0
- jac_scale/abstractions/models/resource_status.jac +38 -0
- jac_scale/config_loader.jac +31 -0
- jac_scale/context.jac +14 -0
- jac_scale/factories/database_factory.jac +43 -0
- jac_scale/factories/deployment_factory.jac +43 -0
- jac_scale/factories/registry_factory.jac +32 -0
- jac_scale/factories/utility_factory.jac +34 -0
- jac_scale/impl/config_loader.impl.jac +131 -0
- jac_scale/impl/context.impl.jac +24 -0
- jac_scale/impl/memory_hierarchy.main.impl.jac +63 -0
- jac_scale/impl/memory_hierarchy.mongo.impl.jac +239 -0
- jac_scale/impl/memory_hierarchy.redis.impl.jac +186 -0
- jac_scale/impl/serve.impl.jac +1785 -0
- jac_scale/jserver/__init__.py +0 -0
- jac_scale/jserver/impl/jfast_api.impl.jac +731 -0
- jac_scale/jserver/impl/jserver.impl.jac +79 -0
- jac_scale/jserver/jfast_api.jac +162 -0
- jac_scale/jserver/jserver.jac +101 -0
- jac_scale/memory_hierarchy.jac +138 -0
- jac_scale/plugin.jac +218 -0
- jac_scale/plugin_config.jac +175 -0
- jac_scale/providers/database/kubernetes_mongo.jac +137 -0
- jac_scale/providers/database/kubernetes_redis.jac +110 -0
- jac_scale/providers/registry/dockerhub.jac +64 -0
- jac_scale/serve.jac +118 -0
- jac_scale/targets/kubernetes/kubernetes_config.jac +215 -0
- jac_scale/targets/kubernetes/kubernetes_target.jac +841 -0
- jac_scale/targets/kubernetes/utils/kubernetes_utils.impl.jac +519 -0
- jac_scale/targets/kubernetes/utils/kubernetes_utils.jac +85 -0
- jac_scale/tests/__init__.py +0 -0
- jac_scale/tests/conftest.py +29 -0
- jac_scale/tests/fixtures/test_api.jac +159 -0
- jac_scale/tests/fixtures/todo_app.jac +68 -0
- jac_scale/tests/test_abstractions.py +88 -0
- jac_scale/tests/test_deploy_k8s.py +265 -0
- jac_scale/tests/test_examples.py +484 -0
- jac_scale/tests/test_factories.py +149 -0
- jac_scale/tests/test_file_upload.py +444 -0
- jac_scale/tests/test_k8s_utils.py +156 -0
- jac_scale/tests/test_memory_hierarchy.py +247 -0
- jac_scale/tests/test_serve.py +1835 -0
- jac_scale/tests/test_sso.py +711 -0
- jac_scale/utilities/loggers/standard_logger.jac +40 -0
- jac_scale/utils.jac +16 -0
- jac_scale-0.1.1.dist-info/METADATA +658 -0
- jac_scale-0.1.1.dist-info/RECORD +57 -0
- jac_scale-0.1.1.dist-info/WHEEL +5 -0
- jac_scale-0.1.1.dist-info/entry_points.txt +3 -0
- 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
|