separan 0.1.0a2__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 (51) hide show
  1. separan/__init__.py +8 -0
  2. separan/__main__.py +3 -0
  3. separan/ast_nodes.py +122 -0
  4. separan/ast_printer.py +22 -0
  5. separan/auth.py +175 -0
  6. separan/browser.py +57 -0
  7. separan/builtins.py +786 -0
  8. separan/bytes_ops.py +89 -0
  9. separan/capabilities.py +77 -0
  10. separan/cli.py +55 -0
  11. separan/collection_ops.py +88 -0
  12. separan/cookie_store.py +97 -0
  13. separan/cookies.py +146 -0
  14. separan/database.py +5 -0
  15. separan/db/__init__.py +5 -0
  16. separan/db/core.py +269 -0
  17. separan/db/drivers/__init__.py +1 -0
  18. separan/db/drivers/_dbapi.py +64 -0
  19. separan/db/drivers/mysql.py +46 -0
  20. separan/db/drivers/oracle.py +69 -0
  21. separan/db/drivers/postgresql.py +44 -0
  22. separan/db/drivers/sqlite.py +98 -0
  23. separan/db/drivers/sqlserver.py +97 -0
  24. separan/db/errors.py +20 -0
  25. separan/db/registry.py +23 -0
  26. separan/errors.py +40 -0
  27. separan/http_client.py +220 -0
  28. separan/http_server.py +208 -0
  29. separan/interpreter.py +572 -0
  30. separan/io_json.py +194 -0
  31. separan/lexer.py +145 -0
  32. separan/list_ops.py +197 -0
  33. separan/lsp.py +382 -0
  34. separan/lsp_analysis.py +222 -0
  35. separan/objects.py +52 -0
  36. separan/parser.py +408 -0
  37. separan/processes.py +118 -0
  38. separan/randomness.py +106 -0
  39. separan/structural.py +303 -0
  40. separan/structure_insights.py +139 -0
  41. separan/system_context.py +54 -0
  42. separan/system_utilities.py +201 -0
  43. separan/temporal.py +263 -0
  44. separan/token.py +95 -0
  45. separan-0.1.0a2.dist-info/METADATA +354 -0
  46. separan-0.1.0a2.dist-info/RECORD +51 -0
  47. separan-0.1.0a2.dist-info/WHEEL +5 -0
  48. separan-0.1.0a2.dist-info/entry_points.txt +4 -0
  49. separan-0.1.0a2.dist-info/licenses/LICENSE +201 -0
  50. separan-0.1.0a2.dist-info/licenses/NOTICE +4 -0
  51. separan-0.1.0a2.dist-info/top_level.txt +1 -0
separan/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """Separan v0.1 reference implementation."""
2
+
3
+ from .interpreter import Interpreter
4
+ from .lexer import Lexer
5
+ from .parser import Parser
6
+
7
+ __all__ = ["Interpreter", "Lexer", "Parser"]
8
+
separan/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
separan/ast_nodes.py ADDED
@@ -0,0 +1,122 @@
1
+ from dataclasses import dataclass, field, fields, is_dataclass
2
+ from typing import Any
3
+
4
+ from .token import SourcePosition
5
+
6
+
7
+ @dataclass
8
+ class Node: position: SourcePosition
9
+ @dataclass
10
+ class Expr(Node): pass
11
+ @dataclass
12
+ class LiteralExpr(Expr): value: Any
13
+ @dataclass
14
+ class VariableExpr(Expr): name: str
15
+ @dataclass
16
+ class BinaryExpr(Expr): left: Expr; operator: str; right: Expr
17
+ @dataclass
18
+ class UnaryExpr(Expr): operator: str; operand: Expr
19
+ @dataclass
20
+ class GroupExpr(Expr): expression: Expr
21
+ @dataclass
22
+ class CallExpr(Expr):
23
+ callee: str
24
+ arguments: list[Expr]
25
+ named_arguments: dict[str, Expr] = field(default_factory=dict)
26
+ @dataclass
27
+ class ListExpr(Expr): elements: list[Expr]
28
+ @dataclass
29
+ class IndexExpr(Expr): target: Expr; index: Expr
30
+ @dataclass
31
+ class MemberExpr(Expr): target: Expr; name: str
32
+ @dataclass
33
+ class MemberCallExpr(Expr): target: Expr; name: str; arguments: list[Expr]; named_arguments: dict[str, Expr] = field(default_factory=dict)
34
+
35
+ @dataclass
36
+ class Stmt(Node): pass
37
+ @dataclass
38
+ class Assignment(Stmt): name: str; value: Expr
39
+ @dataclass
40
+ class ConstDeclaration(Stmt): name: str; value: Expr
41
+ @dataclass
42
+ class PrintStmt(Stmt): value: Expr
43
+ @dataclass
44
+ class PrintErrorStmt(Stmt): value: Expr
45
+ @dataclass
46
+ class ReturnStmt(Stmt): value: Expr | None
47
+ @dataclass
48
+ class ExpressionStmt(Stmt): expression: Expr
49
+ @dataclass
50
+ class ObjectField(Node): name: str; value: Expr
51
+ @dataclass
52
+ class ObjectBlock(Stmt): name: str; entries: list[Node]; label_position: SourcePosition
53
+ @dataclass
54
+ class ListBlock(Stmt): name: str; elements: list[Expr]; label_position: SourcePosition
55
+ @dataclass
56
+ class ImportStmt(Stmt): path: str; alias: str
57
+ @dataclass
58
+ class CatchBranch(Node): category: str; body: list[Stmt]
59
+ @dataclass
60
+ class TryStmt(Stmt): label: str; body: list[Stmt]; catches: list[CatchBranch]; finally_body: list[Stmt] | None; label_position: SourcePosition
61
+ @dataclass
62
+ class ThrowStmt(Stmt): value: Expr
63
+ @dataclass
64
+ class ErrorDecl(Stmt): name: str; label_position: SourcePosition
65
+ @dataclass
66
+ class HttpRouteDecl(Stmt): method: str; path: str; label: str; body: list[Stmt]; label_position: SourcePosition
67
+ @dataclass
68
+ class TransactionStmt(Stmt): connection: Expr; label: str; body: list[Stmt]; label_position: SourcePosition
69
+ @dataclass
70
+ class IfBranch:
71
+ condition: Expr
72
+ body: list[Stmt]
73
+ position: SourcePosition
74
+ @dataclass
75
+ class IfStmt(Stmt):
76
+ label: str
77
+ branches: list[IfBranch]
78
+ else_body: list[Stmt] | None
79
+ label_position: SourcePosition
80
+ @dataclass
81
+ class WhileStmt(Stmt):
82
+ label: str
83
+ condition: Expr
84
+ body: list[Stmt]
85
+ label_position: SourcePosition
86
+ @dataclass
87
+ class ForStmt(Stmt):
88
+ label: str
89
+ variable: str
90
+ iterable: Expr
91
+ body: list[Stmt]
92
+ label_position: SourcePosition
93
+ @dataclass
94
+ class FunctionDecl(Stmt):
95
+ name: str
96
+ parameters: list[str]
97
+ body: list[Stmt]
98
+ label_position: SourcePosition
99
+ @dataclass
100
+ class Program(Node): statements: list[Stmt] = field(default_factory=list)
101
+
102
+
103
+ def ast_structural_equal(left, right):
104
+ """Compare AST meaning while deliberately excluding source locations."""
105
+ return _structural_value(left) == _structural_value(right)
106
+
107
+
108
+ def _structural_value(value):
109
+ if isinstance(value, SourcePosition):
110
+ return None
111
+ if isinstance(value, list):
112
+ return tuple(_structural_value(item) for item in value)
113
+ if is_dataclass(value):
114
+ return (
115
+ type(value),
116
+ tuple(
117
+ (item.name, _structural_value(getattr(value, item.name)))
118
+ for item in fields(value)
119
+ if not isinstance(getattr(value, item.name), SourcePosition)
120
+ ),
121
+ )
122
+ return value
separan/ast_printer.py ADDED
@@ -0,0 +1,22 @@
1
+ from dataclasses import fields, is_dataclass
2
+ from .ast_nodes import Node
3
+ from .token import SourcePosition
4
+
5
+
6
+ def format_ast(node, indent=0):
7
+ pad = " " * indent
8
+ if isinstance(node, list): return "\n".join(format_ast(x, indent) for x in node)
9
+ if not is_dataclass(node): return pad + repr(node)
10
+ title = type(node).__name__
11
+ extras = []
12
+ for name in ("name", "label", "variable", "operator"):
13
+ if hasattr(node, name): extras.append(f"{name}={getattr(node, name)}")
14
+ lines = [pad + title + ((" " + " ".join(extras)) if extras else "")]
15
+ for f in fields(node):
16
+ if f.name in {"position", "label_position", "name", "label", "variable", "operator"}: continue
17
+ value = getattr(node, f.name)
18
+ if isinstance(value, Node) or (isinstance(value, list) and value):
19
+ lines.append(pad + f" {f.name}:")
20
+ lines.append(format_ast(value, indent + 2))
21
+ return "\n".join(lines)
22
+
separan/auth.py ADDED
@@ -0,0 +1,175 @@
1
+ """High-level authentication primitives; no user-defined cryptography surface."""
2
+
3
+ from dataclasses import dataclass
4
+ import base64
5
+ import binascii
6
+ import hashlib
7
+ import hmac
8
+ import json
9
+ import os
10
+ from urllib.parse import urlencode
11
+
12
+ from .errors import error
13
+ from .objects import ObjectValue
14
+ from .randomness import BytesValue
15
+ from .system_utilities import UtilityFunction
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class SecretValue:
20
+ value: bytes
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class HttpAuthValue:
25
+ kind: str
26
+ name: str
27
+ value: SecretValue
28
+ location: str = "header"
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class OAuthTokenValue:
33
+ access_token: SecretValue
34
+ token_type: str
35
+ expires_in: int | None
36
+ scope: str | None
37
+
38
+
39
+ def secret_bytes(value, name, position, runtime):
40
+ if isinstance(value, SecretValue): return value.value
41
+ if isinstance(value, BytesValue): return value.value
42
+ if type(value) is str: return value.encode("utf-8")
43
+ runtime.type_error(position, "secret, bytes, or string", runtime.type_name(value), f"{name} requires secret-compatible input.")
44
+
45
+
46
+ def _secret_get(arguments, named, position, runtime):
47
+ name = arguments[0]
48
+ if type(name) is not str or not name: runtime.type_error(position, "non-empty string", runtime.type_name(name), "secret_get() requires a secret name.")
49
+ capability = runtime.capabilities; capability.require(capability.read_secrets, "read secrets", position)
50
+ if capability.allowed_secrets is not None and name not in capability.allowed_secrets: raise error("E870", "Permission error", "Secret name is outside the host allowlist.", position, actual=name)
51
+ if runtime.secret_provider is None: raise error("E871", "Secret unavailable", "No host secret provider is configured.", position, actual=name)
52
+ try: value = runtime.secret_provider(name)
53
+ except Exception as exc: raise error("E871", "Secret unavailable", str(exc), position, actual=name)
54
+ if value is None: raise error("E871", "Secret unavailable", "The requested secret does not exist.", position, actual=name)
55
+ if type(value) is str: value = value.encode("utf-8")
56
+ if type(value) is not bytes: raise error("E871", "Secret provider error", "Host secret provider must return string, bytes, or null.", position, actual=name)
57
+ return SecretValue(value)
58
+
59
+
60
+ def _basic_auth(arguments, named, position, runtime):
61
+ username = arguments[0]
62
+ if type(username) is not str or ":" in username: raise error("E872", "Invalid Basic auth username", "Username must be a string without ':'.", position)
63
+ password = secret_bytes(arguments[1], "basic_auth() password", position, runtime)
64
+ token = base64.b64encode(username.encode("utf-8") + b":" + password)
65
+ return HttpAuthValue("basic", "Authorization", SecretValue(b"Basic " + token))
66
+
67
+
68
+ def _bearer_auth(arguments, named, position, runtime):
69
+ token = secret_bytes(arguments[0], "bearer_auth() token", position, runtime)
70
+ if not token or any(byte < 33 or byte > 126 for byte in token): raise error("E872", "Invalid bearer token", "Bearer token must contain visible ASCII bytes only.", position)
71
+ return HttpAuthValue("bearer", "Authorization", SecretValue(b"Bearer " + token))
72
+
73
+
74
+ def _api_key_auth(arguments, named, position, runtime):
75
+ name, value = arguments; location = named.get("location", "header")
76
+ if type(name) is not str or not name or any(ord(char) < 33 for char in name): raise error("E872", "Invalid API key name", "API key name must be a safe non-empty string.", position)
77
+ if location not in ("header", "query"): raise error("E872", "Invalid API key location", "API key location must be header or query.", position, actual=repr(location))
78
+ return HttpAuthValue("api_key", name, SecretValue(secret_bytes(value, "api_key_auth() value", position, runtime)), location)
79
+
80
+
81
+ def _hmac_sha256(arguments, named, position, runtime):
82
+ key = secret_bytes(arguments[0], "hmac_sha256() key", position, runtime); message = secret_bytes(arguments[1], "hmac_sha256() message", position, runtime)
83
+ return BytesValue(hmac.new(key, message, hashlib.sha256).digest())
84
+
85
+
86
+ def _json_value(value, position):
87
+ if isinstance(value, ObjectValue): return {key: _json_value(item, position) for key, item in value.fields.items()}
88
+ if type(value) is list: return [_json_value(item, position) for item in value]
89
+ if value is None or type(value) in (str, bool, int, float): return value
90
+ raise error("E875", "Invalid JWT claim", "JWT claims must be JSON-compatible and cannot contain secrets or bytes.", position)
91
+
92
+
93
+ def _b64url(value): return base64.urlsafe_b64encode(value).rstrip(b"=")
94
+ def _b64url_decode(value): return base64.urlsafe_b64decode(value + b"=" * ((4 - len(value) % 4) % 4))
95
+
96
+
97
+ def _jwt_sign(arguments, named, position, runtime):
98
+ claims, key = arguments; algorithm = named.get("algorithm", "HS256")
99
+ if algorithm != "HS256": raise error("E873", "Unsupported JWT algorithm", "Only explicit HS256 is supported in the preview.", position, actual=repr(algorithm))
100
+ if not isinstance(claims, ObjectValue): runtime.type_error(position, "object claims", runtime.type_name(claims), "jwt_sign() claims must be an object.")
101
+ key_bytes = secret_bytes(key, "jwt_sign() key", position, runtime)
102
+ if len(key_bytes) < 32: raise error("E873", "Weak JWT key", "HS256 keys must contain at least 32 bytes.", position)
103
+ header = _b64url(b'{"alg":"HS256","typ":"JWT"}')
104
+ payload = _b64url(json.dumps(_json_value(claims, position), ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8"))
105
+ signing = header + b"." + payload; signature = _b64url(hmac.new(key_bytes, signing, hashlib.sha256).digest())
106
+ return signing.decode("ascii") + "." + signature.decode("ascii")
107
+
108
+
109
+ def _jwt_verify(arguments, named, position, runtime):
110
+ token, key = arguments; algorithm = named.get("algorithm", "HS256")
111
+ if type(token) is not str or algorithm != "HS256": raise error("E873", "Unsupported or invalid JWT", "JWT must be a string and algorithm must be HS256.", position)
112
+ try:
113
+ header_text, payload_text, signature_text = token.encode("ascii").split(b".")
114
+ header = json.loads(_b64url_decode(header_text)); payload = json.loads(_b64url_decode(payload_text))
115
+ key_bytes = secret_bytes(key, "jwt_verify() key", position, runtime)
116
+ if len(key_bytes) < 32: raise ValueError("weak key")
117
+ expected = _b64url(hmac.new(key_bytes, header_text + b"." + payload_text, hashlib.sha256).digest())
118
+ except Exception: raise error("E874", "JWT verification error", "JWT is malformed.", position)
119
+ if header != {"alg": "HS256", "typ": "JWT"} or not hmac.compare_digest(expected, signature_text): raise error("E874", "JWT verification error", "JWT signature or protected header is invalid.", position)
120
+ if type(payload) is not dict: raise error("E874", "JWT verification error", "JWT claims must be an object.", position)
121
+ now = runtime.current_time().timestamp()
122
+ for claim in ("exp", "nbf"):
123
+ if claim in payload and (type(payload[claim]) not in (int, float) or type(payload[claim]) is bool): raise error("E874", "JWT verification error", f"JWT {claim} claim must be a number.", position)
124
+ if "exp" in payload and now >= payload["exp"]: raise error("E874", "JWT verification error", "JWT has expired.", position)
125
+ if "nbf" in payload and now < payload["nbf"]: raise error("E874", "JWT verification error", "JWT is not active yet.", position)
126
+ from .io_json import _from_json
127
+ return _from_json(payload, position)
128
+
129
+
130
+ def _password_hash(arguments, named, position, runtime):
131
+ password = secret_bytes(arguments[0], "password_hash() password", position, runtime); salt = os.urandom(16)
132
+ digest = hashlib.scrypt(password, salt=salt, n=2**14, r=8, p=1, dklen=32)
133
+ return "$separan$scrypt$n=16384,r=8,p=1$" + base64.b64encode(salt).decode() + "$" + base64.b64encode(digest).decode()
134
+
135
+
136
+ def _password_verify(arguments, named, position, runtime):
137
+ password, encoded = arguments
138
+ if type(encoded) is not str: runtime.type_error(position, "password hash string", runtime.type_name(encoded), "password_verify() hash must be a string.")
139
+ try:
140
+ prefix, scheme, params, salt_text, digest_text = encoded.rsplit("$", 4)
141
+ if prefix != "$separan" or scheme != "scrypt" or params != "n=16384,r=8,p=1": return False
142
+ salt = base64.b64decode(salt_text, validate=True); expected = base64.b64decode(digest_text, validate=True)
143
+ actual = hashlib.scrypt(secret_bytes(password, "password_verify() password", position, runtime), salt=salt, n=2**14, r=8, p=1, dklen=32)
144
+ return hmac.compare_digest(actual, expected)
145
+ except (ValueError, binascii.Error): return False
146
+
147
+
148
+ def _oauth_client_credentials(arguments, named, position, runtime):
149
+ token_url, client_id, client_secret = arguments; scope = named.get("scope")
150
+ if type(token_url) is not str or type(client_id) is not str: runtime.type_error(position, "string token URL and client ID", f"{runtime.type_name(token_url)}, {runtime.type_name(client_id)}", "OAuth token URL and client ID must be strings.")
151
+ if scope is not None and type(scope) is not str: runtime.type_error(position, "string or null scope", runtime.type_name(scope), "OAuth scope must be a string or null.")
152
+ form = [("grant_type", "client_credentials")]
153
+ if scope is not None: form.append(("scope", scope))
154
+ headers = ObjectValue.create({"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"})
155
+ from .http_client import _request
156
+ response = _request([token_url], {"method": "POST", "headers": headers, "body": urlencode(form), "auth": _basic_auth([client_id, client_secret], {}, position, runtime)}, position, runtime)
157
+ if not 200 <= response.status < 300 or response.text is None: raise error("E877", "oauth_error", f"OAuth token endpoint returned unusable status {response.status}.", position, actual=str(response.status))
158
+ try: payload = json.loads(response.text)
159
+ except json.JSONDecodeError: raise error("E877", "oauth_error", "OAuth token response is not valid JSON.", position)
160
+ token, token_type = payload.get("access_token"), payload.get("token_type")
161
+ expires, returned_scope = payload.get("expires_in"), payload.get("scope")
162
+ if type(token) is not str or not token or type(token_type) is not str: raise error("E877", "oauth_error", "OAuth response requires string access_token and token_type.", position)
163
+ if expires is not None and (type(expires) is not int or expires < 0): raise error("E877", "oauth_error", "OAuth expires_in must be a non-negative integer.", position)
164
+ if returned_scope is not None and type(returned_scope) is not str: raise error("E877", "oauth_error", "OAuth scope must be a string.", position)
165
+ return OAuthTokenValue(SecretValue(token.encode("utf-8")), token_type, expires, returned_scope)
166
+
167
+
168
+ AUTH_BUILTINS = (
169
+ UtilityFunction("secret_get", 1, 1, _secret_get), UtilityFunction("basic_auth", 2, 2, _basic_auth),
170
+ UtilityFunction("bearer_auth", 1, 1, _bearer_auth), UtilityFunction("api_key_auth", 2, 2, _api_key_auth, ("location",)),
171
+ UtilityFunction("hmac_sha256", 2, 2, _hmac_sha256), UtilityFunction("jwt_sign", 2, 2, _jwt_sign, ("algorithm",)),
172
+ UtilityFunction("jwt_verify", 2, 2, _jwt_verify, ("algorithm",)), UtilityFunction("password_hash", 1, 1, _password_hash),
173
+ UtilityFunction("password_verify", 2, 2, _password_verify),
174
+ UtilityFunction("oauth_client_credentials", 3, 3, _oauth_client_credentials, ("scope",)),
175
+ )
separan/browser.py ADDED
@@ -0,0 +1,57 @@
1
+ """Browser automation boundary for future engine adapters.
2
+
3
+ This module deliberately does not use the HTTP client as a fake browser. A
4
+ conforming adapter must drive a real browser engine and expose JavaScript/DOM
5
+ state through a separate value type.
6
+ """
7
+
8
+ from dataclasses import dataclass
9
+ from typing import Protocol
10
+
11
+
12
+ SUPPORTED_ENGINES = frozenset({"chromium", "firefox", "webkit"})
13
+
14
+
15
+ class BrowserAutomationUnavailable(RuntimeError):
16
+ pass
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class BrowserProfile:
21
+ engine: str = "chromium"
22
+ screen_width: int = 1280
23
+ screen_height: int = 720
24
+ language: str = "en-US"
25
+ headless: bool = True
26
+
27
+ def __post_init__(self):
28
+ if self.engine not in SUPPORTED_ENGINES:
29
+ raise ValueError(f"Unsupported browser engine '{self.engine}'.")
30
+ if isinstance(self.screen_width, bool) or not isinstance(self.screen_width, int) or self.screen_width <= 0:
31
+ raise ValueError("screen_width must be a positive integer.")
32
+ if isinstance(self.screen_height, bool) or not isinstance(self.screen_height, int) or self.screen_height <= 0:
33
+ raise ValueError("screen_height must be a positive integer.")
34
+ if not isinstance(self.language, str) or not self.language.strip():
35
+ raise ValueError("language must be a non-empty string.")
36
+
37
+
38
+ class BrowserPage(Protocol):
39
+ @property
40
+ def url(self) -> str: ...
41
+ def text(self, selector: str) -> str: ...
42
+ def close(self) -> None: ...
43
+
44
+
45
+ class BrowserAdapter(Protocol):
46
+ def open(self, url: str, profile: BrowserProfile) -> BrowserPage: ...
47
+
48
+
49
+ def browser_open(url: str, *, profile: BrowserProfile | None = None,
50
+ adapter: BrowserAdapter | None = None) -> BrowserPage:
51
+ if not isinstance(url, str) or not url.startswith(("http://", "https://")):
52
+ raise ValueError("browser_open requires an absolute HTTP or HTTPS URL.")
53
+ if adapter is None:
54
+ raise BrowserAutomationUnavailable(
55
+ "No browser engine adapter is installed. HTTP retrieval remains available through http_get()."
56
+ )
57
+ return adapter.open(url, profile or BrowserProfile())