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.
- jac_scale/abstractions/config/app_config.jac +5 -2
- jac_scale/config_loader.jac +2 -1
- 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/config_loader.impl.jac +28 -3
- jac_scale/impl/context.impl.jac +1 -0
- jac_scale/impl/serve.impl.jac +749 -266
- jac_scale/impl/user_manager.impl.jac +349 -0
- jac_scale/impl/webhook.impl.jac +212 -0
- jac_scale/jserver/impl/jfast_api.impl.jac +4 -0
- jac_scale/memory_hierarchy.jac +3 -1
- jac_scale/plugin.jac +46 -3
- jac_scale/plugin_config.jac +28 -1
- jac_scale/serve.jac +33 -16
- 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 +89 -0
- jac_scale/tests/fixtures/test_restspec.jac +88 -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 +289 -0
- jac_scale/tests/test_serve.py +411 -4
- 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/webhook.jac +93 -0
- {jac_scale-0.1.1.dist-info → jac_scale-0.1.4.dist-info}/METADATA +11 -4
- {jac_scale-0.1.1.dist-info → jac_scale-0.1.4.dist-info}/RECORD +36 -23
- {jac_scale-0.1.1.dist-info → jac_scale-0.1.4.dist-info}/WHEEL +1 -1
- {jac_scale-0.1.1.dist-info → jac_scale-0.1.4.dist-info}/entry_points.txt +0 -0
- {jac_scale-0.1.1.dist-info → jac_scale-0.1.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""Tests for the storage abstraction."""
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import tempfile
|
|
7
|
+
from collections.abc import Generator
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
from jaclang.runtimelib.storage import LocalStorage # type: ignore[attr-defined]
|
|
14
|
+
except ImportError as e:
|
|
15
|
+
pytest.skip(f"Jac modules not compiled: {e}", allow_module_level=True)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
def temp_storage_dir() -> Generator[str, None, None]:
|
|
20
|
+
"""Create a temporary directory for storage tests."""
|
|
21
|
+
temp_dir = tempfile.mkdtemp()
|
|
22
|
+
yield temp_dir
|
|
23
|
+
if os.path.exists(temp_dir):
|
|
24
|
+
shutil.rmtree(temp_dir)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.fixture
|
|
28
|
+
def local_storage(temp_storage_dir: str) -> Generator[LocalStorage, None, None]:
|
|
29
|
+
"""Create a LocalStorage instance with temp directory."""
|
|
30
|
+
storage = LocalStorage(base_path=temp_storage_dir)
|
|
31
|
+
yield storage
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TestLocalStorage:
|
|
35
|
+
"""Tests for LocalStorage implementation."""
|
|
36
|
+
|
|
37
|
+
def test_upload_from_file_path(
|
|
38
|
+
self, local_storage: LocalStorage, temp_storage_dir: str
|
|
39
|
+
) -> None:
|
|
40
|
+
"""Test uploading a file from a file path."""
|
|
41
|
+
source_file = Path(temp_storage_dir) / "source.txt"
|
|
42
|
+
source_file.write_text("Hello, World!")
|
|
43
|
+
|
|
44
|
+
result = local_storage.upload(str(source_file), "uploaded/file.txt")
|
|
45
|
+
|
|
46
|
+
assert local_storage.exists("uploaded/file.txt")
|
|
47
|
+
assert Path(result).exists()
|
|
48
|
+
|
|
49
|
+
def test_upload_from_file_object(self, local_storage: LocalStorage) -> None:
|
|
50
|
+
"""Test uploading from a file-like object."""
|
|
51
|
+
file_obj = io.BytesIO(b"Binary content here")
|
|
52
|
+
|
|
53
|
+
local_storage.upload(file_obj, "binary/data.bin")
|
|
54
|
+
|
|
55
|
+
assert local_storage.exists("binary/data.bin")
|
|
56
|
+
content = local_storage.download("binary/data.bin")
|
|
57
|
+
assert content == b"Binary content here"
|
|
58
|
+
|
|
59
|
+
def test_download_returns_bytes(self, local_storage: LocalStorage) -> None:
|
|
60
|
+
"""Test download returns bytes when no destination specified."""
|
|
61
|
+
file_obj = io.BytesIO(b"Test content")
|
|
62
|
+
local_storage.upload(file_obj, "test.txt")
|
|
63
|
+
|
|
64
|
+
content = local_storage.download("test.txt")
|
|
65
|
+
|
|
66
|
+
assert content == b"Test content"
|
|
67
|
+
|
|
68
|
+
def test_download_to_file_path(
|
|
69
|
+
self, local_storage: LocalStorage, temp_storage_dir: str
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Test download to a file path."""
|
|
72
|
+
file_obj = io.BytesIO(b"Download me")
|
|
73
|
+
local_storage.upload(file_obj, "source.txt")
|
|
74
|
+
dest_path = Path(temp_storage_dir) / "downloaded.txt"
|
|
75
|
+
|
|
76
|
+
local_storage.download("source.txt", str(dest_path))
|
|
77
|
+
|
|
78
|
+
assert dest_path.exists()
|
|
79
|
+
assert dest_path.read_bytes() == b"Download me"
|
|
80
|
+
|
|
81
|
+
def test_download_to_file_object(self, local_storage: LocalStorage) -> None:
|
|
82
|
+
"""Test download to a file-like object."""
|
|
83
|
+
file_obj = io.BytesIO(b"Stream me")
|
|
84
|
+
local_storage.upload(file_obj, "stream.txt")
|
|
85
|
+
output = io.BytesIO()
|
|
86
|
+
|
|
87
|
+
local_storage.download("stream.txt", output)
|
|
88
|
+
|
|
89
|
+
output.seek(0)
|
|
90
|
+
assert output.read() == b"Stream me"
|
|
91
|
+
|
|
92
|
+
def test_download_nonexistent_file_raises(
|
|
93
|
+
self, local_storage: LocalStorage
|
|
94
|
+
) -> None:
|
|
95
|
+
"""Test that downloading a non-existent file raises FileNotFoundError."""
|
|
96
|
+
with pytest.raises(FileNotFoundError):
|
|
97
|
+
local_storage.download("nonexistent.txt")
|
|
98
|
+
|
|
99
|
+
def test_delete_existing_file(self, local_storage: LocalStorage) -> None:
|
|
100
|
+
"""Test deleting an existing file."""
|
|
101
|
+
file_obj = io.BytesIO(b"Delete me")
|
|
102
|
+
local_storage.upload(file_obj, "to_delete.txt")
|
|
103
|
+
assert local_storage.exists("to_delete.txt")
|
|
104
|
+
|
|
105
|
+
result = local_storage.delete("to_delete.txt")
|
|
106
|
+
|
|
107
|
+
assert result is True
|
|
108
|
+
assert not local_storage.exists("to_delete.txt")
|
|
109
|
+
|
|
110
|
+
def test_delete_nonexistent_file(self, local_storage: LocalStorage) -> None:
|
|
111
|
+
"""Test deleting a non-existent file returns False."""
|
|
112
|
+
result = local_storage.delete("nonexistent.txt")
|
|
113
|
+
|
|
114
|
+
assert result is False
|
|
115
|
+
|
|
116
|
+
def test_exists_returns_true_for_existing(
|
|
117
|
+
self, local_storage: LocalStorage
|
|
118
|
+
) -> None:
|
|
119
|
+
"""Test exists returns True for existing file."""
|
|
120
|
+
file_obj = io.BytesIO(b"I exist")
|
|
121
|
+
local_storage.upload(file_obj, "exists.txt")
|
|
122
|
+
|
|
123
|
+
assert local_storage.exists("exists.txt") is True
|
|
124
|
+
|
|
125
|
+
def test_exists_returns_false_for_nonexistent(
|
|
126
|
+
self, local_storage: LocalStorage
|
|
127
|
+
) -> None:
|
|
128
|
+
"""Test exists returns False for non-existent file."""
|
|
129
|
+
assert local_storage.exists("nonexistent.txt") is False
|
|
130
|
+
|
|
131
|
+
def test_list_files_non_recursive(self, local_storage: LocalStorage) -> None:
|
|
132
|
+
"""Test listing files non-recursively."""
|
|
133
|
+
local_storage.upload(io.BytesIO(b"1"), "folder/file1.txt")
|
|
134
|
+
local_storage.upload(io.BytesIO(b"2"), "folder/file2.txt")
|
|
135
|
+
local_storage.upload(io.BytesIO(b"3"), "folder/sub/file3.txt")
|
|
136
|
+
|
|
137
|
+
files = list(local_storage.list_files("folder", recursive=False))
|
|
138
|
+
|
|
139
|
+
# Should include file1, file2, and sub directory
|
|
140
|
+
assert len(files) == 3
|
|
141
|
+
|
|
142
|
+
def test_list_files_recursive(self, local_storage: LocalStorage) -> None:
|
|
143
|
+
"""Test listing files recursively."""
|
|
144
|
+
local_storage.upload(io.BytesIO(b"1"), "folder/file1.txt")
|
|
145
|
+
local_storage.upload(io.BytesIO(b"2"), "folder/file2.txt")
|
|
146
|
+
local_storage.upload(io.BytesIO(b"3"), "folder/sub/file3.txt")
|
|
147
|
+
|
|
148
|
+
files = list(local_storage.list_files("folder", recursive=True))
|
|
149
|
+
|
|
150
|
+
# Should only include files (not directories) recursively
|
|
151
|
+
assert len(files) == 3
|
|
152
|
+
assert any("file1.txt" in f for f in files)
|
|
153
|
+
assert any("file2.txt" in f for f in files)
|
|
154
|
+
assert any("file3.txt" in f for f in files)
|
|
155
|
+
|
|
156
|
+
def test_get_metadata(self, local_storage: LocalStorage) -> None:
|
|
157
|
+
"""Test getting file metadata."""
|
|
158
|
+
content = b"Metadata test content"
|
|
159
|
+
local_storage.upload(io.BytesIO(content), "meta.txt")
|
|
160
|
+
|
|
161
|
+
metadata = local_storage.get_metadata("meta.txt")
|
|
162
|
+
|
|
163
|
+
assert metadata["size"] == len(content)
|
|
164
|
+
assert "modified" in metadata
|
|
165
|
+
assert "created" in metadata
|
|
166
|
+
assert metadata["is_dir"] is False
|
|
167
|
+
assert metadata["name"] == "meta.txt"
|
|
168
|
+
|
|
169
|
+
def test_get_metadata_nonexistent_raises(self, local_storage: LocalStorage) -> None:
|
|
170
|
+
"""Test that getting metadata of non-existent file raises error."""
|
|
171
|
+
with pytest.raises(FileNotFoundError):
|
|
172
|
+
local_storage.get_metadata("nonexistent.txt")
|
|
173
|
+
|
|
174
|
+
def test_copy_file(self, local_storage: LocalStorage) -> None:
|
|
175
|
+
"""Test copying a file."""
|
|
176
|
+
local_storage.upload(io.BytesIO(b"Copy me"), "original.txt")
|
|
177
|
+
|
|
178
|
+
result = local_storage.copy("original.txt", "copied.txt")
|
|
179
|
+
|
|
180
|
+
assert result is True
|
|
181
|
+
assert local_storage.exists("original.txt")
|
|
182
|
+
assert local_storage.exists("copied.txt")
|
|
183
|
+
assert local_storage.download("copied.txt") == b"Copy me"
|
|
184
|
+
|
|
185
|
+
def test_copy_nonexistent_returns_false(self, local_storage: LocalStorage) -> None:
|
|
186
|
+
"""Test copying non-existent file returns False."""
|
|
187
|
+
result = local_storage.copy("nonexistent.txt", "dest.txt")
|
|
188
|
+
|
|
189
|
+
assert result is False
|
|
190
|
+
|
|
191
|
+
def test_move_file(self, local_storage: LocalStorage) -> None:
|
|
192
|
+
"""Test moving a file."""
|
|
193
|
+
local_storage.upload(io.BytesIO(b"Move me"), "to_move.txt")
|
|
194
|
+
|
|
195
|
+
result = local_storage.move("to_move.txt", "moved.txt")
|
|
196
|
+
|
|
197
|
+
assert result is True
|
|
198
|
+
assert not local_storage.exists("to_move.txt")
|
|
199
|
+
assert local_storage.exists("moved.txt")
|
|
200
|
+
assert local_storage.download("moved.txt") == b"Move me"
|
|
201
|
+
|
|
202
|
+
def test_move_nonexistent_returns_false(self, local_storage: LocalStorage) -> None:
|
|
203
|
+
"""Test moving non-existent file returns False."""
|
|
204
|
+
result = local_storage.move("nonexistent.txt", "dest.txt")
|
|
205
|
+
|
|
206
|
+
assert result is False
|
|
207
|
+
|
|
208
|
+
def test_creates_directories_automatically(self, temp_storage_dir: str) -> None:
|
|
209
|
+
"""Test that directories are created when create_dirs is True."""
|
|
210
|
+
new_path = os.path.join(temp_storage_dir, "new", "nested", "dir")
|
|
211
|
+
LocalStorage(base_path=new_path, create_dirs=True)
|
|
212
|
+
|
|
213
|
+
assert os.path.exists(new_path)
|
|
214
|
+
|
|
215
|
+
def test_upload_creates_parent_directories(
|
|
216
|
+
self, local_storage: LocalStorage
|
|
217
|
+
) -> None:
|
|
218
|
+
"""Test that upload creates parent directories as needed."""
|
|
219
|
+
file_obj = io.BytesIO(b"Nested content")
|
|
220
|
+
|
|
221
|
+
local_storage.upload(file_obj, "deep/nested/folder/file.txt")
|
|
222
|
+
|
|
223
|
+
assert local_storage.exists("deep/nested/folder/file.txt")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class TestStorageIntegration:
|
|
227
|
+
"""Integration tests for storage operations."""
|
|
228
|
+
|
|
229
|
+
def test_full_file_lifecycle(self, local_storage: LocalStorage) -> None:
|
|
230
|
+
"""Test complete file lifecycle: upload, read, copy, move, delete."""
|
|
231
|
+
# Upload
|
|
232
|
+
content = b"Lifecycle test content"
|
|
233
|
+
local_storage.upload(io.BytesIO(content), "lifecycle.txt")
|
|
234
|
+
assert local_storage.exists("lifecycle.txt")
|
|
235
|
+
|
|
236
|
+
# Read
|
|
237
|
+
downloaded = local_storage.download("lifecycle.txt")
|
|
238
|
+
assert downloaded == content
|
|
239
|
+
|
|
240
|
+
# Copy
|
|
241
|
+
local_storage.copy("lifecycle.txt", "lifecycle_copy.txt")
|
|
242
|
+
assert local_storage.exists("lifecycle_copy.txt")
|
|
243
|
+
|
|
244
|
+
# Move
|
|
245
|
+
local_storage.move("lifecycle_copy.txt", "lifecycle_moved.txt")
|
|
246
|
+
assert not local_storage.exists("lifecycle_copy.txt")
|
|
247
|
+
assert local_storage.exists("lifecycle_moved.txt")
|
|
248
|
+
|
|
249
|
+
# Delete
|
|
250
|
+
local_storage.delete("lifecycle.txt")
|
|
251
|
+
local_storage.delete("lifecycle_moved.txt")
|
|
252
|
+
assert not local_storage.exists("lifecycle.txt")
|
|
253
|
+
assert not local_storage.exists("lifecycle_moved.txt")
|
|
254
|
+
|
|
255
|
+
def test_upload_large_file(self, local_storage: LocalStorage) -> None:
|
|
256
|
+
"""Test uploading a larger file (1MB)."""
|
|
257
|
+
large_content = b"x" * (1024 * 1024) # 1MB
|
|
258
|
+
file_obj = io.BytesIO(large_content)
|
|
259
|
+
|
|
260
|
+
local_storage.upload(file_obj, "large_file.bin")
|
|
261
|
+
|
|
262
|
+
metadata = local_storage.get_metadata("large_file.bin")
|
|
263
|
+
assert metadata["size"] == 1024 * 1024
|
|
264
|
+
|
|
265
|
+
downloaded = local_storage.download("large_file.bin")
|
|
266
|
+
assert downloaded == large_content
|
|
267
|
+
|
|
268
|
+
def test_special_characters_in_filename(self, local_storage: LocalStorage) -> None:
|
|
269
|
+
"""Test handling files with special characters in name."""
|
|
270
|
+
content = b"Special chars"
|
|
271
|
+
local_storage.upload(io.BytesIO(content), "file-with_special.chars.txt")
|
|
272
|
+
|
|
273
|
+
assert local_storage.exists("file-with_special.chars.txt")
|
|
274
|
+
assert local_storage.download("file-with_special.chars.txt") == content
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import jwt;
|
|
2
|
+
import from datetime { UTC, datetime, timedelta }
|
|
3
|
+
import from typing { Any }
|
|
4
|
+
import from fastapi { Request, Response }
|
|
5
|
+
import from jaclang.runtimelib.server { UserManager }
|
|
6
|
+
import from jaclang.runtimelib.transport { TransportResponse, Meta }
|
|
7
|
+
import from jac_scale.serve { Platforms, Operations }
|
|
8
|
+
import from jac_scale.utils { generate_random_password }
|
|
9
|
+
import from jac_scale.config_loader { get_scale_config }
|
|
10
|
+
import from jac_scale.sso_provider { SSOProvider, SSOUserInfo }
|
|
11
|
+
import from jac_scale.google_sso_provider { GoogleSSOProvider }
|
|
12
|
+
|
|
13
|
+
# Load configuration
|
|
14
|
+
glob _jwt_config = get_scale_config().get_jwt_config(),
|
|
15
|
+
_sso_config = get_scale_config().get_sso_config(),
|
|
16
|
+
JWT_SECRET = _jwt_config['secret'],
|
|
17
|
+
JWT_ALGORITHM = _jwt_config['algorithm'],
|
|
18
|
+
JWT_EXP_DELTA_DAYS = _jwt_config['exp_delta_days'],
|
|
19
|
+
SSO_HOST = _sso_config['host'];
|
|
20
|
+
|
|
21
|
+
obj JacScaleUserManager(UserManager) {
|
|
22
|
+
has SUPPORTED_PLATFORMS: dict = {};
|
|
23
|
+
|
|
24
|
+
def postinit -> None;
|
|
25
|
+
# JWT methods
|
|
26
|
+
def create_jwt_token(username: str) -> str;
|
|
27
|
+
def validate_jwt_token(token: str) -> (str | None);
|
|
28
|
+
def refresh_jwt_token(token: str) -> (str | None);
|
|
29
|
+
# SSO methods
|
|
30
|
+
def get_sso(platform: str, operation: str) -> (SSOProvider | None);
|
|
31
|
+
async def sso_initiate(
|
|
32
|
+
platform: str, operation: str
|
|
33
|
+
) -> (Response | TransportResponse);
|
|
34
|
+
|
|
35
|
+
async def sso_callback(
|
|
36
|
+
request: Request, platform: str, operation: str
|
|
37
|
+
) -> TransportResponse;
|
|
38
|
+
|
|
39
|
+
# SSO Account Linking methods
|
|
40
|
+
def link_sso_account(
|
|
41
|
+
user_id: str, platform: str, external_id: str, email: str
|
|
42
|
+
) -> dict[str, str];
|
|
43
|
+
|
|
44
|
+
def unlink_sso_account(user_id: str, platform: str) -> dict[str, str];
|
|
45
|
+
def get_sso_accounts(user_id: str) -> list[dict[str, str]];
|
|
46
|
+
def get_user_by_sso(platform: str, external_id: str) -> (dict[str, str] | None);
|
|
47
|
+
# Override validate_token to use JWT
|
|
48
|
+
def validate_token(token: str) -> (str | None);
|
|
49
|
+
}
|
jac_scale/webhook.jac
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Webhook support for Jac Scale.
|
|
2
|
+
|
|
3
|
+
This module provides webhook-related functionality including:
|
|
4
|
+
- API key generation and management
|
|
5
|
+
- HMAC-SHA256 signature verification for webhook security
|
|
6
|
+
- Webhook configuration and utilities
|
|
7
|
+
|
|
8
|
+
API keys are generated per-user and can be used to authenticate webhook calls.
|
|
9
|
+
The API key is wrapped in a JWT for secure storage and validation.
|
|
10
|
+
"""
|
|
11
|
+
import hmac;
|
|
12
|
+
import hashlib;
|
|
13
|
+
import secrets;
|
|
14
|
+
import jwt;
|
|
15
|
+
import from datetime { UTC, datetime, timedelta }
|
|
16
|
+
import from typing { Any }
|
|
17
|
+
import from pydantic { BaseModel, Field }
|
|
18
|
+
import from jaclang.runtimelib.transport { TransportResponse, Meta }
|
|
19
|
+
import from jac_scale.config_loader { get_scale_config }
|
|
20
|
+
|
|
21
|
+
# Load webhook configuration
|
|
22
|
+
glob _webhook_config = get_scale_config().get_webhook_config(),
|
|
23
|
+
WEBHOOK_SECRET = _webhook_config['secret'],
|
|
24
|
+
WEBHOOK_SIGNATURE_HEADER = _webhook_config['signature_header'],
|
|
25
|
+
WEBHOOK_VERIFY_SIGNATURE = _webhook_config['verify_signature'],
|
|
26
|
+
WEBHOOK_API_KEY_EXPIRY_DAYS = _webhook_config['api_key_expiry_days'];
|
|
27
|
+
|
|
28
|
+
"""Pydantic model for API key creation request."""
|
|
29
|
+
class CreateApiKeyRequest(BaseModel) {
|
|
30
|
+
has name: str = Field(..., description='A friendly name for the API key'),
|
|
31
|
+
expiry_days: int | None = Field(
|
|
32
|
+
None, description='Number of days until expiry (default from config)'
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
"""Pydantic model for API key response."""
|
|
37
|
+
class ApiKeyResponse(BaseModel) {
|
|
38
|
+
has api_key: str = Field(..., description='The generated API key'),
|
|
39
|
+
api_key_id: str = Field(..., description='Unique identifier for the API key'),
|
|
40
|
+
name: str = Field(..., description='Friendly name for the API key'),
|
|
41
|
+
created_at: str = Field(..., description='ISO timestamp of creation'),
|
|
42
|
+
expires_at: str | None = Field(
|
|
43
|
+
None, description='ISO timestamp of expiry, or null if no expiry'
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
"""Webhook utilities for signature generation and verification."""
|
|
48
|
+
obj WebhookUtils {
|
|
49
|
+
"""Generate HMAC-SHA256 signature for webhook payload."""
|
|
50
|
+
static def generate_signature(payload: bytes, secret: str) -> str;
|
|
51
|
+
|
|
52
|
+
"""Verify HMAC-SHA256 signature for webhook payload."""
|
|
53
|
+
static def verify_signature(payload: bytes, signature: str, secret: str) -> bool;
|
|
54
|
+
|
|
55
|
+
"""Generate a new API key."""
|
|
56
|
+
static def generate_api_key -> str;
|
|
57
|
+
|
|
58
|
+
"""Create a JWT-wrapped API key token.
|
|
59
|
+
|
|
60
|
+
The API key is embedded in a JWT for secure validation and expiry management."""
|
|
61
|
+
static def create_api_key_token(
|
|
62
|
+
api_key_id: str, username: str, name: str, expiry_days: int | None = None
|
|
63
|
+
) -> str;
|
|
64
|
+
|
|
65
|
+
"""Validate an API key token and extract user information."""
|
|
66
|
+
static def validate_api_key(api_key: str) -> dict[str, str] | None;
|
|
67
|
+
|
|
68
|
+
"""Extract signature from request header value."""
|
|
69
|
+
static def extract_signature(header_value: str) -> str;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
"""Manager for API keys associated with users."""
|
|
73
|
+
obj ApiKeyManager {
|
|
74
|
+
has _api_keys: dict[str, dict[str, Any]] = {}, # api_key_id -> key info
|
|
75
|
+
_user_keys: dict[str, list[str]] = {}; # username -> list of api_key_ids
|
|
76
|
+
|
|
77
|
+
"""Create a new API key for a user."""
|
|
78
|
+
def create_api_key(
|
|
79
|
+
username: str, name: str, expiry_days: int | None = None
|
|
80
|
+
) -> TransportResponse;
|
|
81
|
+
|
|
82
|
+
"""List all API keys for a user."""
|
|
83
|
+
def list_api_keys(username: str) -> TransportResponse;
|
|
84
|
+
|
|
85
|
+
"""Revoke an API key."""
|
|
86
|
+
def revoke_api_key(username: str, api_key_id: str) -> TransportResponse;
|
|
87
|
+
|
|
88
|
+
"""Validate an API key and return the associated username."""
|
|
89
|
+
def validate_api_key(api_key: str) -> str | None;
|
|
90
|
+
|
|
91
|
+
"""Check if an API key ID exists and is not revoked."""
|
|
92
|
+
def is_key_active(api_key_id: str) -> bool;
|
|
93
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jac-scale
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Author-email: Jason Mars <jason@mars.ninja>
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
7
|
-
Requires-Dist: jaclang>=0.9.
|
|
7
|
+
Requires-Dist: jaclang>=0.9.13
|
|
8
8
|
Requires-Dist: python-dotenv<2.0.0,>=1.2.1
|
|
9
9
|
Requires-Dist: docker<8.0.0,>=7.1.0
|
|
10
10
|
Requires-Dist: kubernetes<35.0.0,>=34.1.0
|
|
@@ -12,7 +12,7 @@ Requires-Dist: pymongo<5.0.0,>=4.15.4
|
|
|
12
12
|
Requires-Dist: redis<8.0.0,>=7.1.0
|
|
13
13
|
Requires-Dist: fastapi<0.122.0,>=0.121.3
|
|
14
14
|
Requires-Dist: uvicorn<0.39.0,>=0.38.0
|
|
15
|
-
Requires-Dist: pyjwt
|
|
15
|
+
Requires-Dist: pyjwt<2.11.0,>=2.10.1
|
|
16
16
|
Requires-Dist: fastapi-sso<1.0.0,>=0.18.0
|
|
17
17
|
Requires-Dist: python-multipart<1.0.0,>=0.0.21
|
|
18
18
|
|
|
@@ -43,6 +43,13 @@ Requires-Dist: python-multipart<1.0.0,>=0.0.21
|
|
|
43
43
|
|
|
44
44
|
Whether you're developing locally with `jac start` or deploying to production with `jac start --scale`, you get the same powerful features with the flexibility to choose your deployment strategy.
|
|
45
45
|
|
|
46
|
+
### 4. Single Sign-On (SSO) Support
|
|
47
|
+
|
|
48
|
+
- **Google SSO**: Built-in support for Google Sign-In out of the box
|
|
49
|
+
- **Extensible Architecture**: Easily add other providers (GitHub, Microsoft, etc.)
|
|
50
|
+
- **Secure Authentication**: Integrated with JWT for secure session management
|
|
51
|
+
- **User Management**: Automatic account creation and linking
|
|
52
|
+
|
|
46
53
|
## Prerequisites
|
|
47
54
|
|
|
48
55
|
- kubenetes(K8s) installed
|
|
@@ -484,7 +491,7 @@ async walker FetchData {
|
|
|
484
491
|
| `MONGODB_URI` | URL of MongoDB database | - |
|
|
485
492
|
| `REDIS_URL` | URL of Redis database | - |
|
|
486
493
|
| `JWT_EXP_DELTA_DAYS` | Number of days until JWT token expires | `7` |
|
|
487
|
-
| `JWT_SECRET` | Secret key used for JWT token signing and verification | `'
|
|
494
|
+
| `JWT_SECRET` | Secret key used for JWT token signing and verification | `'supersecretkey_for_testing_only!'` |
|
|
488
495
|
| `JWT_ALGORITHM` | Algorithm used for JWT token encoding/decoding | `'HS256'` |
|
|
489
496
|
| `SSO_HOST` | SSO host URL | `'http://localhost:8000/sso'` |
|
|
490
497
|
| `SSO_GOOGLE_CLIENT_ID` | Google OAuth client ID | - |
|
|
@@ -1,57 +1,70 @@
|
|
|
1
1
|
jac_scale/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
jac_scale/config_loader.jac,sha256=
|
|
3
|
-
jac_scale/context.jac,sha256=
|
|
4
|
-
jac_scale/
|
|
5
|
-
jac_scale/
|
|
6
|
-
jac_scale/
|
|
7
|
-
jac_scale/
|
|
2
|
+
jac_scale/config_loader.jac,sha256=fG1dN9AS54bzNL5XvPHNS4oMw8kknZnlAZ7ByPT0APU,1298
|
|
3
|
+
jac_scale/context.jac,sha256=SIqQineAlLHJV3yWtYkeTIfcnqS0Oj15n8CWVSIqwJ8,529
|
|
4
|
+
jac_scale/google_sso_provider.jac,sha256=UUTDgQrBHXp0eqHxiw7v_VkQyFMMza0UDM6S4T5e4OM,2473
|
|
5
|
+
jac_scale/memory_hierarchy.jac,sha256=GuydhwujsH71TAkxeJ6Wfutp4v6GjDj23vGMreRj4yg,4533
|
|
6
|
+
jac_scale/plugin.jac,sha256=E1Ou_sQ8RV35emY9-gM295Uu-Eg4w4qSEJYm3TTYsWY,9585
|
|
7
|
+
jac_scale/plugin_config.jac,sha256=6_cTMzjDhr-7fGh2RMM5LCfjXlfKduGyFs0S0PnHKXQ,8718
|
|
8
|
+
jac_scale/serve.jac,sha256=8xMOHX3X-sJFx6yENolL-7VM7Km1Mg5BPkUtMLUl9O4,5546
|
|
9
|
+
jac_scale/sso_provider.jac,sha256=Kky8cxm5gR2Sbg-CFrWTILKSZrV6Ju-DCEgfG4Akxb8,2092
|
|
10
|
+
jac_scale/user_manager.jac,sha256=UBApt_znhVEyxtfAdJ9sp_gslkHel4GGwXCBcNen_as,1932
|
|
8
11
|
jac_scale/utils.jac,sha256=zpxA08_NlDDSd1oVlbknQ_UOwHWGBbhzjmT_u6UYN04,491
|
|
12
|
+
jac_scale/webhook.jac,sha256=lTpevPoe-CEcj0-MAd8c1fHK7jGUL3YnN4_ouMeq4Ic,3660
|
|
9
13
|
jac_scale/abstractions/database_provider.jac,sha256=NZbFmcESulYsmG-27f8GY9nxVs3JflGIEb3aDDOIPu0,1454
|
|
10
14
|
jac_scale/abstractions/deployment_target.jac,sha256=5rJOd25FUXZZCNmDSUkyMljcXTAVJJDmelKw8WdPZm0,2071
|
|
11
15
|
jac_scale/abstractions/image_registry.jac,sha256=UCOwwuts5e1emsdDIlHJOiyW2ePA7k7D8UJdzd2kN10,1386
|
|
12
16
|
jac_scale/abstractions/logger.jac,sha256=YnXn8_jCpZ0IOBkJ7bFBMqo6WXO6BaDNVBqPq0lHbjk,652
|
|
13
|
-
jac_scale/abstractions/config/app_config.jac,sha256=
|
|
17
|
+
jac_scale/abstractions/config/app_config.jac,sha256=yxXlTOFRNBq5yFMgYm1LjEzobwxP_k2EG4zXcoHf688,912
|
|
14
18
|
jac_scale/abstractions/config/base_config.jac,sha256=ePT-up-63QGu2Jifi_CpPhd7_V9cPcHkInqGKDFryC4,785
|
|
15
19
|
jac_scale/abstractions/models/deployment_result.jac,sha256=3Mx8TVvL74AF8LLnhMbuCyHTTu-DXesYWMvht1HoGFs,704
|
|
16
20
|
jac_scale/abstractions/models/resource_status.jac,sha256=H6I9ZF92ia56bNmHR9dLMGsaFG4BafGh4sY-YwVRPu0,1031
|
|
17
21
|
jac_scale/factories/database_factory.jac,sha256=-8QfF1vDUqsdDKnldmayxvB-Z6DwQeNMOlo7HgTaouQ,1736
|
|
18
22
|
jac_scale/factories/deployment_factory.jac,sha256=1pzQ2HkqpKaeoycbRxdZ05McgTLpgj5Y5hmKhFTm1AY,1673
|
|
19
23
|
jac_scale/factories/registry_factory.jac,sha256=qWgwEH75EtjXGj3d9NGeg7D9Yx2S_RW9udlCFvOOCqU,1210
|
|
24
|
+
jac_scale/factories/storage_factory.jac,sha256=FVlx-f1BOq6H1yLWXT7OLSG-ZCENvlBPNr0OwFdo-VU,2661
|
|
20
25
|
jac_scale/factories/utility_factory.jac,sha256=64gMv_N_pFeq5E7z3gXrLIqeRGS_RGIF5eLT8RxNl_I,1262
|
|
21
|
-
jac_scale/impl/config_loader.impl.jac,sha256=
|
|
22
|
-
jac_scale/impl/context.impl.jac,sha256=
|
|
26
|
+
jac_scale/impl/config_loader.impl.jac,sha256=gIcOowYuoMvpilRK6vhAGf5ULYttrkh19fB5zP7KTdI,5668
|
|
27
|
+
jac_scale/impl/context.impl.jac,sha256=3M33iA9L2SCIMT0rQTvb8MkveNCuTEjc7rNknK4Hruk,1071
|
|
23
28
|
jac_scale/impl/memory_hierarchy.main.impl.jac,sha256=Jc4Cy-5cy0TtK2uQ6rdYruxa0ZfZKSYgo3jOoXv4ER4,2339
|
|
24
29
|
jac_scale/impl/memory_hierarchy.mongo.impl.jac,sha256=APh7YloOj7bWGtv6DTBm7ekX13w_fectbZiFY0btwWU,6626
|
|
25
30
|
jac_scale/impl/memory_hierarchy.redis.impl.jac,sha256=x27TzeBI87vz72s7esQvFz8EPk3EfhVMNKU8sP4s2hk,4900
|
|
26
|
-
jac_scale/impl/serve.impl.jac,sha256=
|
|
31
|
+
jac_scale/impl/serve.impl.jac,sha256=zzWAF0PukDMdKKFmiwP-cupxrCA8YtZ_leYY-Qzcnkc,85767
|
|
32
|
+
jac_scale/impl/user_manager.impl.jac,sha256=7xGoLMweQuoVW-na7XbK3bctfUHaSEnT9nB7DLgSlgw,11817
|
|
33
|
+
jac_scale/impl/webhook.impl.jac,sha256=87IZn9nfSaNXX9RYXVJRJEz-3xzznpC6RhVBoJJA2X8,7504
|
|
27
34
|
jac_scale/jserver/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
28
35
|
jac_scale/jserver/jfast_api.jac,sha256=Ez7EZ883TS9IIACzK8QMqTgVTtNoaOenbmwNKnekBuc,5561
|
|
29
36
|
jac_scale/jserver/jserver.jac,sha256=8gWh22nX9Z5rJp3D1AxOSU8lBhoX4-kBZ0uvJPPNqX8,3356
|
|
30
|
-
jac_scale/jserver/impl/jfast_api.impl.jac,sha256=
|
|
37
|
+
jac_scale/jserver/impl/jfast_api.impl.jac,sha256=bnxoReOOntQL0FHDgLSoaXtQRtCYOOEVWhq-lAXeqI0,26931
|
|
31
38
|
jac_scale/jserver/impl/jserver.impl.jac,sha256=4NOzfys4WQmGt6vttTTh6By12hEJqdvOzCZyCBsUQKk,2224
|
|
32
39
|
jac_scale/providers/database/kubernetes_mongo.jac,sha256=nP43b-ePR8XInhhk7gzdvJvOJMZcTZanm1VALdiCqXA,4952
|
|
33
40
|
jac_scale/providers/database/kubernetes_redis.jac,sha256=qF9HkNLLgiymxiCXbmETbVVDs5Wqs1m4s3O6xFR7jRA,3778
|
|
34
41
|
jac_scale/providers/registry/dockerhub.jac,sha256=7KlgQJGGUeDF7zP8-KNF8HTShB4td1946OtIdS57EHI,2282
|
|
35
|
-
jac_scale/targets/kubernetes/kubernetes_config.jac,sha256=
|
|
36
|
-
jac_scale/targets/kubernetes/kubernetes_target.jac,sha256=
|
|
42
|
+
jac_scale/targets/kubernetes/kubernetes_config.jac,sha256=Xk3EjKTEjptlctD2C5cTYjgw9V8i8mfsO-jKAa2IZZs,9406
|
|
43
|
+
jac_scale/targets/kubernetes/kubernetes_target.jac,sha256=itICKs_sA4jOs7_wrFgpj5QhupjKVXRSvkaJMBL6t9g,35523
|
|
37
44
|
jac_scale/targets/kubernetes/utils/kubernetes_utils.impl.jac,sha256=CPSlFlhCE3qdzPWiPVa7KbHkL_h5Gle5ySvq7UkwR4k,15756
|
|
38
45
|
jac_scale/targets/kubernetes/utils/kubernetes_utils.jac,sha256=Nr257oCyduodJbxkLJeNYphFzz9NxviFfW1Tpc5dQKk,2129
|
|
39
46
|
jac_scale/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
40
47
|
jac_scale/tests/conftest.py,sha256=uiS352_HVmMf7V6T0BZl-Kn8svkKbj3jhNr-hEKrcPw,820
|
|
41
48
|
jac_scale/tests/test_abstractions.py,sha256=gb5ueNcUcxpq0U0r_nEize9v027gRR_XiZlCEooAW6U,2612
|
|
42
|
-
jac_scale/tests/test_deploy_k8s.py,sha256=
|
|
43
|
-
jac_scale/tests/test_examples.py,sha256=
|
|
49
|
+
jac_scale/tests/test_deploy_k8s.py,sha256=rX9kJohqTeaRch-KSruWYF8eUnPGtJ_p6zy0sPkBXIA,9164
|
|
50
|
+
jac_scale/tests/test_examples.py,sha256=2xOTf0rBiT_9w92ku7k9Pfv4A7ohLZpZdpAH0VHM86E,24044
|
|
44
51
|
jac_scale/tests/test_factories.py,sha256=cf1gLrBdK9q8SlHyocZqtixyPtO2dLlrgNYeX5-_RYM,5073
|
|
45
52
|
jac_scale/tests/test_file_upload.py,sha256=JooF-9rgwOqtNkWRfvfHu4W5x-gzk91bHVi13J9mnNE,16595
|
|
53
|
+
jac_scale/tests/test_hooks.py,sha256=TzikM6dd1uKg1G2L1MtgXYzzjY6YhrbAQXZah-TO33Y,1366
|
|
46
54
|
jac_scale/tests/test_k8s_utils.py,sha256=Oqocwl4m2IFgM27Lgsi96WJeHS8hnx9WDOcxq9YkqJU,4613
|
|
47
55
|
jac_scale/tests/test_memory_hierarchy.py,sha256=q_DsKyPIuQymab_sEtXojDW0GOyunRxZJsNTJDrGA7s,8324
|
|
48
|
-
jac_scale/tests/
|
|
49
|
-
jac_scale/tests/
|
|
50
|
-
jac_scale/tests/
|
|
56
|
+
jac_scale/tests/test_restspec.py,sha256=fYNpQi7ZhV7KnSoJQ4xH1tJIQeayaeCvFS5GBLAZO1o,10853
|
|
57
|
+
jac_scale/tests/test_serve.py,sha256=xKPjQJLdtWaCISlP3Kirf3A4i1Fxubej_h5oLklKSY8,81244
|
|
58
|
+
jac_scale/tests/test_sso.py,sha256=9h2AUC9JBM74O6xfpyIcl98iXyZWcDIGdKRtlb_5uqo,28104
|
|
59
|
+
jac_scale/tests/test_storage.py,sha256=Y7CIbKrH3OmJQlBeV9A965-7TtxL6nuZtfkfoeqIeS4,10514
|
|
60
|
+
jac_scale/tests/fixtures/test_api.jac,sha256=aqfvy4wgc0qPTM9_NYvXUcpemNTSdwW0tp432sdHZI4,6680
|
|
61
|
+
jac_scale/tests/fixtures/test_restspec.jac,sha256=oFqACGtuoGz1xsURAaeXrQe7hkMI-W8hL5IKFDTQHKU,2511
|
|
51
62
|
jac_scale/tests/fixtures/todo_app.jac,sha256=beI6AiRutmXsXxuzU9nIZTE-AuoBBP-WqbRA2ux1pN4,1216
|
|
63
|
+
jac_scale/tests/fixtures/scale-feats/main.jac,sha256=QN8Opodha9jLWdRGCASByIogePr1XKphgKjyE9Pp73c,4205
|
|
64
|
+
jac_scale/tests/fixtures/scale-feats/components/Button.cl.jac,sha256=e8tvNzW_QikFJMSTcbkziKP69NF00DqQVxcjLPkAW-w,903
|
|
52
65
|
jac_scale/utilities/loggers/standard_logger.jac,sha256=6XL5ETAOBwbsFCOp0VN_7TOnqcQDmbLZVzubA-JR3vA,1376
|
|
53
|
-
jac_scale-0.1.
|
|
54
|
-
jac_scale-0.1.
|
|
55
|
-
jac_scale-0.1.
|
|
56
|
-
jac_scale-0.1.
|
|
57
|
-
jac_scale-0.1.
|
|
66
|
+
jac_scale-0.1.4.dist-info/METADATA,sha256=F_sIgqYZVaPGz7cPpx-Sc-EqI0cNX43nEkFWQiUjU5w,20491
|
|
67
|
+
jac_scale-0.1.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
68
|
+
jac_scale-0.1.4.dist-info/entry_points.txt,sha256=n-Wm8JEtGOqy_IY_kgIOi3-uYnuVK-iWsvKiLkxlG4E,105
|
|
69
|
+
jac_scale-0.1.4.dist-info/top_level.txt,sha256=PpgR0R8z9qoFbSser2K20r5Is4K6TxVwguoN6LfTEKU,10
|
|
70
|
+
jac_scale-0.1.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|