ngapakin 1.0.0__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.
- framework/__init__.py +5 -0
- framework/auth.py +75 -0
- framework/cache.py +72 -0
- framework/container.py +42 -0
- framework/controller.py +51 -0
- framework/database.py +58 -0
- framework/env.py +44 -0
- framework/event.py +30 -0
- framework/middleware.py +62 -0
- framework/model.py +232 -0
- framework/package_manager.py +127 -0
- framework/query_builder.py +105 -0
- framework/queue.py +145 -0
- framework/request.py +47 -0
- framework/response.py +60 -0
- framework/router.py +74 -0
- framework/schema.py +52 -0
- framework/session.py +85 -0
- framework/template_engine.py +283 -0
- framework/validation.py +100 -0
- framework/view.py +8 -0
- ngapak/__init__.py +9 -0
- ngapak/ast.py +186 -0
- ngapak/builtins.py +76 -0
- ngapak/bytecode.py +34 -0
- ngapak/compiler.py +580 -0
- ngapak/debugger.py +138 -0
- ngapak/environment.py +63 -0
- ngapak/errors.py +28 -0
- ngapak/frame.py +10 -0
- ngapak/interpreter.py +236 -0
- ngapak/lexer.py +193 -0
- ngapak/opcode.py +41 -0
- ngapak/parser.py +417 -0
- ngapak/token.py +90 -0
- ngapak/vm.py +579 -0
- ngapakin-1.0.0.dist-info/METADATA +230 -0
- ngapakin-1.0.0.dist-info/RECORD +43 -0
- ngapakin-1.0.0.dist-info/WHEEL +5 -0
- ngapakin-1.0.0.dist-info/entry_points.txt +2 -0
- ngapakin-1.0.0.dist-info/licenses/LICENSE +21 -0
- ngapakin-1.0.0.dist-info/top_level.txt +3 -0
- ngapakin.py +905 -0
framework/__init__.py
ADDED
framework/auth.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Authentication System for Larapak
|
|
2
|
+
|
|
3
|
+
from framework.database import db_manager
|
|
4
|
+
|
|
5
|
+
class Auth:
|
|
6
|
+
def __init__(self, vm=None):
|
|
7
|
+
self.vm = vm
|
|
8
|
+
|
|
9
|
+
@property
|
|
10
|
+
def sesi(self):
|
|
11
|
+
if self.vm and "sesi" in self.vm.globals:
|
|
12
|
+
return self.vm.globals["sesi"]
|
|
13
|
+
return None
|
|
14
|
+
|
|
15
|
+
def user(self):
|
|
16
|
+
ses = self.sesi
|
|
17
|
+
if not ses:
|
|
18
|
+
return None
|
|
19
|
+
user_id = ses.get("auth_user_id")
|
|
20
|
+
if not user_id:
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
if self.vm and "User" in self.vm.globals:
|
|
24
|
+
User = self.vm.globals["User"]
|
|
25
|
+
# Call User.golek(user_id)
|
|
26
|
+
# golek is a classmethod on Model, which wraps it.
|
|
27
|
+
# In Python, we can call it using __func__ since Model.golek is a classmethod
|
|
28
|
+
from framework.model import Model
|
|
29
|
+
return Model.golek.__func__(User, user_id)
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
def cek(self):
|
|
33
|
+
return self.user() is not None
|
|
34
|
+
|
|
35
|
+
def check(self):
|
|
36
|
+
return self.cek()
|
|
37
|
+
|
|
38
|
+
def mlebu(self, email, password):
|
|
39
|
+
"""Login user with email and password."""
|
|
40
|
+
if self.vm and "User" in self.vm.globals:
|
|
41
|
+
User = self.vm.globals["User"]
|
|
42
|
+
from framework.model import Model
|
|
43
|
+
# Query user using ModelQueryBuilder
|
|
44
|
+
# We will make sure Model.nek works and returns a user.
|
|
45
|
+
# Or we can query directly via SQL for safety inside auth:
|
|
46
|
+
table = Model.get_table_name(User)
|
|
47
|
+
row = db_manager.execute(f"SELECT * FROM {table} WHERE email = ?", [email]).fetchone()
|
|
48
|
+
if row:
|
|
49
|
+
user_dict = dict(row)
|
|
50
|
+
if user_dict.get("password") == password:
|
|
51
|
+
ses = self.sesi
|
|
52
|
+
if ses:
|
|
53
|
+
ses.set("auth_user_id", user_dict.get("id"))
|
|
54
|
+
|
|
55
|
+
# Instantiate user object
|
|
56
|
+
from ngapak.vm import NgapakInstance
|
|
57
|
+
instance = User() if hasattr(User, 'attributes') else User.__new__(User)
|
|
58
|
+
if isinstance(instance, NgapakInstance):
|
|
59
|
+
instance.fields.update(user_dict)
|
|
60
|
+
else:
|
|
61
|
+
instance.__dict__.update(user_dict)
|
|
62
|
+
return True
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
def login(self, email, password):
|
|
66
|
+
return self.mlebu(email, password)
|
|
67
|
+
|
|
68
|
+
def metu(self):
|
|
69
|
+
"""Logout current user."""
|
|
70
|
+
ses = self.sesi
|
|
71
|
+
if ses:
|
|
72
|
+
ses.forget("auth_user_id")
|
|
73
|
+
|
|
74
|
+
def logout(self):
|
|
75
|
+
self.metu()
|
framework/cache.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Cache Manager for Larapak
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
import json
|
|
5
|
+
from framework.database import db_manager
|
|
6
|
+
|
|
7
|
+
class Cache:
|
|
8
|
+
def __init__(self):
|
|
9
|
+
self._ensure_table()
|
|
10
|
+
|
|
11
|
+
def _ensure_table(self):
|
|
12
|
+
db_manager.execute_write(
|
|
13
|
+
"CREATE TABLE IF NOT EXISTS cache (key VARCHAR(255) PRIMARY KEY, value TEXT, expiration INTEGER)"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
def pasang(self, key, value, seconds=None):
|
|
17
|
+
expiration = None
|
|
18
|
+
if seconds is not None:
|
|
19
|
+
expiration = int(time.time() + seconds)
|
|
20
|
+
|
|
21
|
+
serialized = json.dumps(value)
|
|
22
|
+
db_manager.execute_write(
|
|
23
|
+
"INSERT OR REPLACE INTO cache (key, value, expiration) VALUES (?, ?, ?)",
|
|
24
|
+
[key, serialized, expiration]
|
|
25
|
+
)
|
|
26
|
+
return True
|
|
27
|
+
|
|
28
|
+
def set(self, key, value, seconds=None):
|
|
29
|
+
return self.pasang(key, value, seconds)
|
|
30
|
+
|
|
31
|
+
def entuk(self, key, default=None):
|
|
32
|
+
row = db_manager.execute("SELECT value, expiration FROM cache WHERE key = ?", [key]).fetchone()
|
|
33
|
+
if not row:
|
|
34
|
+
return default
|
|
35
|
+
|
|
36
|
+
expiration = row["expiration"]
|
|
37
|
+
if expiration is not None and expiration <= time.time():
|
|
38
|
+
# Expired, remove it
|
|
39
|
+
self.lalekna(key)
|
|
40
|
+
return default
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
return json.loads(row["value"])
|
|
44
|
+
except Exception:
|
|
45
|
+
return default
|
|
46
|
+
|
|
47
|
+
def get(self, key, default=None):
|
|
48
|
+
return self.entuk(key, default)
|
|
49
|
+
|
|
50
|
+
def lalekna(self, key):
|
|
51
|
+
db_manager.execute_write("DELETE FROM cache WHERE key = ?", [key])
|
|
52
|
+
return True
|
|
53
|
+
|
|
54
|
+
def forget(self, key):
|
|
55
|
+
return self.lalekna(key)
|
|
56
|
+
|
|
57
|
+
def remove(self, key):
|
|
58
|
+
return self.lalekna(key)
|
|
59
|
+
|
|
60
|
+
def resiki(self):
|
|
61
|
+
db_manager.execute_write("DELETE FROM cache")
|
|
62
|
+
return True
|
|
63
|
+
|
|
64
|
+
def clear(self):
|
|
65
|
+
return self.resiki()
|
|
66
|
+
|
|
67
|
+
def nduwe(self, key):
|
|
68
|
+
val = self.entuk(key, None)
|
|
69
|
+
return val is not None
|
|
70
|
+
|
|
71
|
+
def has(self, key):
|
|
72
|
+
return self.nduwe(key)
|
framework/container.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Service Container for Larapak Web Framework
|
|
2
|
+
|
|
3
|
+
class Container:
|
|
4
|
+
def __init__(self):
|
|
5
|
+
self.bindings = {}
|
|
6
|
+
self.instances = {}
|
|
7
|
+
self.vm = None # Injected by VM
|
|
8
|
+
|
|
9
|
+
def bind(self, name, factory):
|
|
10
|
+
"""Bind a dependency resolver function to a name."""
|
|
11
|
+
self.bindings[name] = factory
|
|
12
|
+
|
|
13
|
+
def singleton(self, name, factory):
|
|
14
|
+
"""Bind a singleton dependency resolver."""
|
|
15
|
+
def singleton_factory():
|
|
16
|
+
if name not in self.instances:
|
|
17
|
+
if self.vm and callable(factory):
|
|
18
|
+
# If it's a VM function, invoke it
|
|
19
|
+
self.instances[name] = self.vm.execute_callable(factory, [])
|
|
20
|
+
else:
|
|
21
|
+
self.instances[name] = factory()
|
|
22
|
+
return self.instances[name]
|
|
23
|
+
self.bindings[name] = singleton_factory
|
|
24
|
+
|
|
25
|
+
def make(self, name):
|
|
26
|
+
"""Resolve dependency by name."""
|
|
27
|
+
if name in self.instances:
|
|
28
|
+
return self.instances[name]
|
|
29
|
+
|
|
30
|
+
if name in self.bindings:
|
|
31
|
+
factory = self.bindings[name]
|
|
32
|
+
# Check if it is a VM function
|
|
33
|
+
if self.vm and hasattr(factory, 'chunk'):
|
|
34
|
+
res = self.vm.execute_callable(factory, [])
|
|
35
|
+
return res
|
|
36
|
+
elif callable(factory):
|
|
37
|
+
res = factory()
|
|
38
|
+
return res
|
|
39
|
+
else:
|
|
40
|
+
return factory
|
|
41
|
+
|
|
42
|
+
raise KeyError(f"Dependency '{name}' tidak ditemukan di Service Container.")
|
framework/controller.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Controller resolution and loader for Larapak
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from ngapak import Lexer, Parser, Compiler, NgapakError
|
|
5
|
+
|
|
6
|
+
class ControllerLoader:
|
|
7
|
+
def __init__(self, controllers_path, vm):
|
|
8
|
+
self.controllers_path = controllers_path
|
|
9
|
+
self.vm = vm
|
|
10
|
+
self.loaded_controllers = {}
|
|
11
|
+
|
|
12
|
+
def call_action(self, action_str, req, res):
|
|
13
|
+
"""Invoke controller action method (e.g. 'HomeController@index') on VM."""
|
|
14
|
+
parts = action_str.split("@")
|
|
15
|
+
if len(parts) < 2:
|
|
16
|
+
raise ValueError(f"Format aksi controller tidak valid: '{action_str}'")
|
|
17
|
+
|
|
18
|
+
controller_name = parts[0]
|
|
19
|
+
action_name = parts[1]
|
|
20
|
+
|
|
21
|
+
# Load and run controller file once to register its functions in VM globals
|
|
22
|
+
if controller_name not in self.loaded_controllers:
|
|
23
|
+
filepath = os.path.join(self.controllers_path, f"{controller_name}.ngpk")
|
|
24
|
+
if not os.path.exists(filepath):
|
|
25
|
+
raise FileNotFoundError(f"Controller '{controller_name}' tidak ditemukan di '{filepath}'")
|
|
26
|
+
|
|
27
|
+
with open(filepath, "r", encoding="utf-8") as f:
|
|
28
|
+
source = f.read()
|
|
29
|
+
|
|
30
|
+
lexer = Lexer(source, filepath)
|
|
31
|
+
tokens = lexer.tokenize()
|
|
32
|
+
parser = Parser(tokens, filepath)
|
|
33
|
+
ast = parser.parse()
|
|
34
|
+
|
|
35
|
+
compiler = Compiler(filepath)
|
|
36
|
+
main_fn = compiler.compile(ast)
|
|
37
|
+
|
|
38
|
+
# Execute the controller file main scope to define functions
|
|
39
|
+
self.vm.execute_callable(main_fn, [])
|
|
40
|
+
self.loaded_controllers[controller_name] = True
|
|
41
|
+
|
|
42
|
+
# Retrieve action function from VM globals
|
|
43
|
+
if action_name not in self.vm.globals:
|
|
44
|
+
raise AttributeError(f"Aksi '{action_name}' tidak ditemukan di Controller '{controller_name}'.")
|
|
45
|
+
|
|
46
|
+
action_fn = self.vm.globals[action_name]
|
|
47
|
+
|
|
48
|
+
# Call the controller function on VM: action(req, res)
|
|
49
|
+
# Returns the Response object modified or returned by the function
|
|
50
|
+
result_response = self.vm.execute_callable(action_fn, [req, res])
|
|
51
|
+
return result_response
|
framework/database.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Database Connection & Transaction Manager for Larapak
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sqlite3
|
|
5
|
+
|
|
6
|
+
class DatabaseConnection:
|
|
7
|
+
def __init__(self, db_path="database.sqlite"):
|
|
8
|
+
self.db_path = db_path
|
|
9
|
+
self._conn = None
|
|
10
|
+
self.query_log = []
|
|
11
|
+
self.log_queries = True
|
|
12
|
+
|
|
13
|
+
def get_connection(self):
|
|
14
|
+
if self._conn is None:
|
|
15
|
+
# Ensure folder exists
|
|
16
|
+
db_dir = os.path.dirname(self.db_path)
|
|
17
|
+
if db_dir and not os.path.exists(db_dir):
|
|
18
|
+
os.makedirs(db_dir, exist_ok=True)
|
|
19
|
+
self._conn = sqlite3.connect(self.db_path, check_same_thread=False)
|
|
20
|
+
self._conn.row_factory = sqlite3.Row
|
|
21
|
+
return self._conn
|
|
22
|
+
|
|
23
|
+
def execute(self, sql, params=None):
|
|
24
|
+
params = params or []
|
|
25
|
+
if self.log_queries:
|
|
26
|
+
print(f"[SQL] {sql} | Params: {params}")
|
|
27
|
+
self.query_log.append((sql, params))
|
|
28
|
+
|
|
29
|
+
conn = self.get_connection()
|
|
30
|
+
cursor = conn.cursor()
|
|
31
|
+
cursor.execute(sql, params)
|
|
32
|
+
return cursor
|
|
33
|
+
|
|
34
|
+
def execute_write(self, sql, params=None):
|
|
35
|
+
params = params or []
|
|
36
|
+
cursor = self.execute(sql, params)
|
|
37
|
+
conn = self.get_connection()
|
|
38
|
+
conn.commit()
|
|
39
|
+
return cursor.lastrowid
|
|
40
|
+
|
|
41
|
+
def begin_transaction(self):
|
|
42
|
+
self.execute("BEGIN")
|
|
43
|
+
|
|
44
|
+
def commit(self):
|
|
45
|
+
self.execute("COMMIT")
|
|
46
|
+
|
|
47
|
+
def rollback(self):
|
|
48
|
+
self.execute("ROLLBACK")
|
|
49
|
+
|
|
50
|
+
def close(self):
|
|
51
|
+
if self._conn:
|
|
52
|
+
self._conn.close()
|
|
53
|
+
self._conn = None
|
|
54
|
+
|
|
55
|
+
from .env import env
|
|
56
|
+
|
|
57
|
+
# Global db connection
|
|
58
|
+
db_manager = DatabaseConnection(env("DB_DATABASE", "database.sqlite"))
|
framework/env.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Environment Configuration Loader for Larapak (.env)
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
_ENV_LOADED = False
|
|
6
|
+
|
|
7
|
+
def load_env(env_file=".env"):
|
|
8
|
+
"""Loads key-value pairs from .env file into os.environ."""
|
|
9
|
+
global _ENV_LOADED
|
|
10
|
+
if not os.path.exists(env_file):
|
|
11
|
+
return
|
|
12
|
+
|
|
13
|
+
with open(env_file, "r", encoding="utf-8") as f:
|
|
14
|
+
for line in f:
|
|
15
|
+
line = line.strip()
|
|
16
|
+
# Skip empty lines and comments
|
|
17
|
+
if not line or line.startswith("#"):
|
|
18
|
+
continue
|
|
19
|
+
if "=" in line:
|
|
20
|
+
key, val = line.split("=", 1)
|
|
21
|
+
key = key.strip()
|
|
22
|
+
val = val.strip()
|
|
23
|
+
# Strip wrapping single or double quotes
|
|
24
|
+
if (val.startswith('"') and val.endswith('"')) or (val.startswith("'") and val.endswith("'")):
|
|
25
|
+
val = val[1:-1]
|
|
26
|
+
# Only set if not already overridden by system environment
|
|
27
|
+
if key not in os.environ:
|
|
28
|
+
os.environ[key] = val
|
|
29
|
+
|
|
30
|
+
_ENV_LOADED = True
|
|
31
|
+
|
|
32
|
+
def env(key, default=None):
|
|
33
|
+
"""Retrieves an environment variable with optional fallback default."""
|
|
34
|
+
if not _ENV_LOADED:
|
|
35
|
+
load_env()
|
|
36
|
+
val = os.environ.get(key, default)
|
|
37
|
+
# Type conversions for standard boolean and null strings
|
|
38
|
+
if val == "bener" or val == "true" or val == "True":
|
|
39
|
+
return True
|
|
40
|
+
if val == "salah" or val == "false" or val == "False":
|
|
41
|
+
return False
|
|
42
|
+
if val == "kosong" or val == "null" or val == "None":
|
|
43
|
+
return None
|
|
44
|
+
return val
|
framework/event.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Event Dispatcher for Larapak
|
|
2
|
+
|
|
3
|
+
class Event:
|
|
4
|
+
def __init__(self, vm=None):
|
|
5
|
+
self.vm = vm
|
|
6
|
+
self.listeners = {}
|
|
7
|
+
|
|
8
|
+
def rungokna(self, event_name, listener):
|
|
9
|
+
if event_name not in self.listeners:
|
|
10
|
+
self.listeners[event_name] = []
|
|
11
|
+
self.listeners[event_name].append(listener)
|
|
12
|
+
|
|
13
|
+
def listen(self, event_name, listener):
|
|
14
|
+
self.rungokna(event_name, listener)
|
|
15
|
+
|
|
16
|
+
def kirim(self, event_name, data=None):
|
|
17
|
+
if event_name not in self.listeners:
|
|
18
|
+
return
|
|
19
|
+
|
|
20
|
+
for listener in self.listeners[event_name]:
|
|
21
|
+
# Call the listener
|
|
22
|
+
if hasattr(listener, 'chunk'):
|
|
23
|
+
# Ngapak VM Function
|
|
24
|
+
if self.vm:
|
|
25
|
+
self.vm.execute_callable(listener, [data] if data is not None else [])
|
|
26
|
+
elif callable(listener):
|
|
27
|
+
listener(data) if data is not None else listener()
|
|
28
|
+
|
|
29
|
+
def dispatch(self, event_name, data=None):
|
|
30
|
+
self.kirim(event_name, data)
|
framework/middleware.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Middleware pipeline execution for Larapak
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from ngapak import Lexer, Parser, Compiler, NgapakError
|
|
5
|
+
|
|
6
|
+
class MiddlewarePipeline:
|
|
7
|
+
def __init__(self, middleware_path, vm):
|
|
8
|
+
self.middleware_path = middleware_path
|
|
9
|
+
self.vm = vm
|
|
10
|
+
self.middleware_map = {
|
|
11
|
+
"auth": "AuthMiddleware" # Map shorthand aliases
|
|
12
|
+
}
|
|
13
|
+
self.loaded_middleware = {}
|
|
14
|
+
|
|
15
|
+
def execute_chain(self, middleware_names, req, res, destination):
|
|
16
|
+
"""Execute middleware list sequentially, ending at destination controller call."""
|
|
17
|
+
idx = 0
|
|
18
|
+
|
|
19
|
+
def next_step(current_req, current_res):
|
|
20
|
+
nonlocal idx
|
|
21
|
+
if idx < len(middleware_names):
|
|
22
|
+
name = middleware_names[idx]
|
|
23
|
+
idx += 1
|
|
24
|
+
return self.run_middleware(name, current_req, current_res, next_step)
|
|
25
|
+
else:
|
|
26
|
+
return destination(current_req, current_res)
|
|
27
|
+
|
|
28
|
+
return next_step(req, res)
|
|
29
|
+
|
|
30
|
+
def run_middleware(self, name, req, res, next_cb):
|
|
31
|
+
class_name = self.middleware_map.get(name, name)
|
|
32
|
+
|
|
33
|
+
# Load and run middleware file once to register its functions
|
|
34
|
+
if class_name not in self.loaded_middleware:
|
|
35
|
+
filepath = os.path.join(self.middleware_path, f"{class_name}.ngpk")
|
|
36
|
+
if not os.path.exists(filepath):
|
|
37
|
+
raise FileNotFoundError(f"Middleware '{class_name}' tidak ditemukan di '{filepath}'")
|
|
38
|
+
|
|
39
|
+
with open(filepath, "r", encoding="utf-8") as f:
|
|
40
|
+
source = f.read()
|
|
41
|
+
|
|
42
|
+
lexer = Lexer(source, filepath)
|
|
43
|
+
tokens = lexer.tokenize()
|
|
44
|
+
parser = Parser(tokens, filepath)
|
|
45
|
+
ast = parser.parse()
|
|
46
|
+
|
|
47
|
+
compiler = Compiler(filepath)
|
|
48
|
+
main_fn = compiler.compile(ast)
|
|
49
|
+
|
|
50
|
+
self.vm.execute_callable(main_fn, [])
|
|
51
|
+
self.loaded_middleware[class_name] = True
|
|
52
|
+
|
|
53
|
+
# Middlewares define a global function called 'tangani(req, res, lanjut)'
|
|
54
|
+
if "tangani" not in self.vm.globals:
|
|
55
|
+
return next_cb(req, res)
|
|
56
|
+
|
|
57
|
+
tangani_fn = self.vm.globals["tangani"]
|
|
58
|
+
|
|
59
|
+
# Run middleware tangani(req, res, next_cb) on VM.
|
|
60
|
+
# next_cb is passed directly as Python callback.
|
|
61
|
+
res_obj = self.vm.execute_callable(tangani_fn, [req, res, next_cb])
|
|
62
|
+
return res_obj
|
framework/model.py
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# Base ORM Model for Larapak (Eloquent-like)
|
|
2
|
+
|
|
3
|
+
from .database import db_manager
|
|
4
|
+
from .query_builder import QueryBuilder
|
|
5
|
+
from ngapak.vm import NgapakClass, NgapakInstance
|
|
6
|
+
|
|
7
|
+
class Model:
|
|
8
|
+
active_vm = None
|
|
9
|
+
tabel = None
|
|
10
|
+
fillable = []
|
|
11
|
+
|
|
12
|
+
@classmethod
|
|
13
|
+
def get_table_name(cls, class_obj):
|
|
14
|
+
if isinstance(class_obj, NgapakClass):
|
|
15
|
+
return class_obj.attributes.get("tabel", f"{class_obj.name.lower()}s")
|
|
16
|
+
return getattr(class_obj, "tabel", f"{class_obj.__name__.lower()}s")
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def get_fillable(cls, class_obj):
|
|
20
|
+
if isinstance(class_obj, NgapakClass):
|
|
21
|
+
return class_obj.attributes.get("fillable", [])
|
|
22
|
+
return getattr(class_obj, "fillable", [])
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def kabeh(cls):
|
|
26
|
+
# cls is the class object (e.g. NgapakClass 'User' or Model subclass)
|
|
27
|
+
table = Model.get_table_name(cls)
|
|
28
|
+
rows = QueryBuilder(table).kabeh()
|
|
29
|
+
|
|
30
|
+
instances = []
|
|
31
|
+
for row in rows:
|
|
32
|
+
instance = cls() if isinstance(cls, NgapakClass) else cls.__new__(cls)
|
|
33
|
+
if isinstance(instance, NgapakInstance):
|
|
34
|
+
instance.fields.update(row)
|
|
35
|
+
else:
|
|
36
|
+
instance.__dict__.update(row)
|
|
37
|
+
instances.append(instance)
|
|
38
|
+
return instances
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def golek(cls, record_id):
|
|
42
|
+
table = Model.get_table_name(cls)
|
|
43
|
+
row = QueryBuilder(table).nek("id", "==", record_id).golek()
|
|
44
|
+
if not row:
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
instance = cls() if isinstance(cls, NgapakClass) else cls.__new__(cls)
|
|
48
|
+
if isinstance(instance, NgapakInstance):
|
|
49
|
+
instance.fields.update(row)
|
|
50
|
+
else:
|
|
51
|
+
instance.__dict__.update(row)
|
|
52
|
+
return instance
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def gawe(cls, data):
|
|
56
|
+
table = Model.get_table_name(cls)
|
|
57
|
+
fillable = Model.get_fillable(cls)
|
|
58
|
+
|
|
59
|
+
# Filter fields based on fillable
|
|
60
|
+
filtered_data = {}
|
|
61
|
+
if fillable:
|
|
62
|
+
for k, v in data.items():
|
|
63
|
+
if k in fillable:
|
|
64
|
+
filtered_data[k] = v
|
|
65
|
+
else:
|
|
66
|
+
filtered_data = data
|
|
67
|
+
|
|
68
|
+
record_id = QueryBuilder(table).tambah(filtered_data)
|
|
69
|
+
|
|
70
|
+
# Reload instance from DB to get autogenerated fields/defaults
|
|
71
|
+
return Model.golek.__func__(cls, record_id)
|
|
72
|
+
|
|
73
|
+
# Instance Methods
|
|
74
|
+
def simpen(self):
|
|
75
|
+
# self is NgapakInstance or Model subclass
|
|
76
|
+
cls = self.ngapak_class if isinstance(self, NgapakInstance) else self.__class__
|
|
77
|
+
table = Model.get_table_name(cls)
|
|
78
|
+
|
|
79
|
+
fields = self.fields if isinstance(self, NgapakInstance) else self.__dict__
|
|
80
|
+
|
|
81
|
+
if "id" in fields and fields["id"] is not None:
|
|
82
|
+
# Update
|
|
83
|
+
QueryBuilder(table).nek("id", "==", fields["id"]).anyari(fields)
|
|
84
|
+
else:
|
|
85
|
+
# Insert
|
|
86
|
+
record_id = QueryBuilder(table).tambah(fields)
|
|
87
|
+
fields["id"] = record_id
|
|
88
|
+
return self
|
|
89
|
+
|
|
90
|
+
def busak(self):
|
|
91
|
+
cls = self.ngapak_class if isinstance(self, NgapakInstance) else self.__class__
|
|
92
|
+
table = Model.get_table_name(cls)
|
|
93
|
+
fields = self.fields if isinstance(self, NgapakInstance) else self.__dict__
|
|
94
|
+
|
|
95
|
+
if "id" in fields and fields["id"] is not None:
|
|
96
|
+
QueryBuilder(table).nek("id", "==", fields["id"]).busak()
|
|
97
|
+
fields["id"] = None
|
|
98
|
+
return self
|
|
99
|
+
|
|
100
|
+
# Relationship Methods
|
|
101
|
+
def hasOne(self, related_class_name, foreign_key=None, local_key="id"):
|
|
102
|
+
if not Model.active_vm:
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
related_class = Model.active_vm.globals.get(related_class_name)
|
|
106
|
+
if not related_class:
|
|
107
|
+
raise ValueError(f"Model '{related_class_name}' tidak ditemukan di VM globals.")
|
|
108
|
+
|
|
109
|
+
related_table = Model.get_table_name(related_class)
|
|
110
|
+
|
|
111
|
+
# Resolve foreign key
|
|
112
|
+
cls = self.ngapak_class if isinstance(self, NgapakInstance) else self.__class__
|
|
113
|
+
if foreign_key is None:
|
|
114
|
+
foreign_key = f"{cls.name.lower()}_id"
|
|
115
|
+
|
|
116
|
+
fields = self.fields if isinstance(self, NgapakInstance) else self.__dict__
|
|
117
|
+
local_val = fields.get(local_key)
|
|
118
|
+
|
|
119
|
+
row = QueryBuilder(related_table).nek(foreign_key, "==", local_val).golek()
|
|
120
|
+
if not row:
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
instance = related_class() if isinstance(related_class, NgapakClass) else related_class.__new__(related_class)
|
|
124
|
+
if isinstance(instance, NgapakInstance):
|
|
125
|
+
instance.fields.update(row)
|
|
126
|
+
else:
|
|
127
|
+
instance.__dict__.update(row)
|
|
128
|
+
return instance
|
|
129
|
+
|
|
130
|
+
def hasMany(self, related_class_name, foreign_key=None, local_key="id"):
|
|
131
|
+
if not Model.active_vm:
|
|
132
|
+
return []
|
|
133
|
+
|
|
134
|
+
related_class = Model.active_vm.globals.get(related_class_name)
|
|
135
|
+
if not related_class:
|
|
136
|
+
raise ValueError(f"Model '{related_class_name}' tidak ditemukan di VM globals.")
|
|
137
|
+
|
|
138
|
+
related_table = Model.get_table_name(related_class)
|
|
139
|
+
|
|
140
|
+
# Resolve foreign key
|
|
141
|
+
cls = self.ngapak_class if isinstance(self, NgapakInstance) else self.__class__
|
|
142
|
+
if foreign_key is None:
|
|
143
|
+
foreign_key = f"{cls.name.lower()}_id"
|
|
144
|
+
|
|
145
|
+
fields = self.fields if isinstance(self, NgapakInstance) else self.__dict__
|
|
146
|
+
local_val = fields.get(local_key)
|
|
147
|
+
|
|
148
|
+
rows = QueryBuilder(related_table).nek(foreign_key, "==", local_val).kabeh()
|
|
149
|
+
|
|
150
|
+
instances = []
|
|
151
|
+
for row in rows:
|
|
152
|
+
instance = related_class() if isinstance(related_class, NgapakClass) else related_class.__new__(related_class)
|
|
153
|
+
if isinstance(instance, NgapakInstance):
|
|
154
|
+
instance.fields.update(row)
|
|
155
|
+
else:
|
|
156
|
+
instance.__dict__.update(row)
|
|
157
|
+
instances.append(instance)
|
|
158
|
+
return instances
|
|
159
|
+
|
|
160
|
+
def belongsTo(self, related_class_name, foreign_key=None, owner_key="id"):
|
|
161
|
+
if not Model.active_vm:
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
related_class = Model.active_vm.globals.get(related_class_name)
|
|
165
|
+
if not related_class:
|
|
166
|
+
raise ValueError(f"Model '{related_class_name}' tidak ditemukan di VM globals.")
|
|
167
|
+
|
|
168
|
+
related_table = Model.get_table_name(related_class)
|
|
169
|
+
|
|
170
|
+
# Resolve foreign key
|
|
171
|
+
if foreign_key is None:
|
|
172
|
+
foreign_key = f"{related_class_name.lower()}_id"
|
|
173
|
+
|
|
174
|
+
fields = self.fields if isinstance(self, NgapakInstance) else self.__dict__
|
|
175
|
+
foreign_val = fields.get(foreign_key)
|
|
176
|
+
|
|
177
|
+
row = QueryBuilder(related_table).nek(owner_key, "==", foreign_val).golek()
|
|
178
|
+
if not row:
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
instance = related_class() if isinstance(related_class, NgapakClass) else related_class.__new__(related_class)
|
|
182
|
+
if isinstance(instance, NgapakInstance):
|
|
183
|
+
instance.fields.update(row)
|
|
184
|
+
else:
|
|
185
|
+
instance.__dict__.update(row)
|
|
186
|
+
return instance
|
|
187
|
+
|
|
188
|
+
@classmethod
|
|
189
|
+
def nek(cls, kolom, op, nilai=None):
|
|
190
|
+
table = Model.get_table_name(cls)
|
|
191
|
+
qb = ModelQueryBuilder(cls, table)
|
|
192
|
+
return qb.nek(kolom, op, nilai)
|
|
193
|
+
|
|
194
|
+
@classmethod
|
|
195
|
+
def neng(cls, kolom, op, nilai=None):
|
|
196
|
+
return cls.nek(kolom, op, nilai)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class ModelQueryBuilder(QueryBuilder):
|
|
200
|
+
def __init__(self, cls, table):
|
|
201
|
+
super().__init__(table)
|
|
202
|
+
self.cls = cls
|
|
203
|
+
|
|
204
|
+
def siji(self):
|
|
205
|
+
row = self.golek()
|
|
206
|
+
if not row:
|
|
207
|
+
return None
|
|
208
|
+
instance = self.cls() if isinstance(self.cls, NgapakClass) else self.cls.__new__(self.cls)
|
|
209
|
+
if isinstance(instance, NgapakInstance):
|
|
210
|
+
instance.fields.update(row)
|
|
211
|
+
else:
|
|
212
|
+
instance.__dict__.update(row)
|
|
213
|
+
return instance
|
|
214
|
+
|
|
215
|
+
def first(self):
|
|
216
|
+
return self.siji()
|
|
217
|
+
|
|
218
|
+
def kabeh(self):
|
|
219
|
+
rows = super().kabeh()
|
|
220
|
+
instances = []
|
|
221
|
+
for row in rows:
|
|
222
|
+
instance = self.cls() if isinstance(self.cls, NgapakClass) else self.cls.__new__(self.cls)
|
|
223
|
+
if isinstance(instance, NgapakInstance):
|
|
224
|
+
instance.fields.update(row)
|
|
225
|
+
else:
|
|
226
|
+
instance.__dict__.update(row)
|
|
227
|
+
instances.append(instance)
|
|
228
|
+
return instances
|
|
229
|
+
|
|
230
|
+
def all(self):
|
|
231
|
+
return self.kabeh()
|
|
232
|
+
|